diff --git a/artifacts/planet_services_preview.png b/artifacts/planet_services_preview.png new file mode 100644 index 0000000..199d85a Binary files /dev/null and b/artifacts/planet_services_preview.png differ diff --git a/artifacts/planet_services_preview.png.import b/artifacts/planet_services_preview.png.import new file mode 100644 index 0000000..0af500e --- /dev/null +++ b/artifacts/planet_services_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdy2tv4him7u4" +path="res://.godot/imported/planet_services_preview.png-30cacc45a7eb1c9740376c061502ca8d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/planet_services_preview.png" +dest_files=["res://.godot/imported/planet_services_preview.png-30cacc45a7eb1c9740376c061502ca8d.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/resources/items/gamma_cannon.tres b/resources/items/gamma_cannon.tres index 389adca..4db6f96 100644 --- a/resources/items/gamma_cannon.tres +++ b/resources/items/gamma_cannon.tres @@ -7,7 +7,7 @@ icon = ExtResource("icon") item_id = "gamma_cannon" display_name = "Działko gamma" category = 0 -description = "Radiacyjna kula. Napromieniowanie: 3 s, 25% obrażeń trafienia/s. Kolejne trafienia odświeżają czas. Zużywa napromieniowaną amunicję." +description = "Radiacyjna kula. Napromieniowanie: 10 s, 25% obrażeń trafienia/s. Kolejne trafienia odświeżają czas. Zużywa napromieniowaną amunicję." primary_stat = "damage" primary_value = 15.0 stat_suffix = " DMG" diff --git a/scenes/ui/planet_services_menu.tscn b/scenes/ui/planet_services_menu.tscn index 4712420..56663b1 100644 --- a/scenes/ui/planet_services_menu.tscn +++ b/scenes/ui/planet_services_menu.tscn @@ -3,16 +3,16 @@ [ext_resource type="Script" path="res://scripts/ui/planet_services_menu.gd" id="1_services"] [sub_resource type="StyleBoxFlat" id="PanelStyle"] -bg_color = Color(0.045, 0.075, 0.11, 0.64) +bg_color = Color(0.025, 0.045, 0.065, 0.94) border_width_left = 1 border_width_top = 1 border_width_right = 1 border_width_bottom = 1 border_color = Color(0.28, 0.63, 0.54, 1) -content_margin_left = 14.0 -content_margin_top = 14.0 -content_margin_right = 14.0 -content_margin_bottom = 14.0 +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 corner_radius_top_left = 8 corner_radius_top_right = 8 corner_radius_bottom_left = 8 @@ -21,7 +21,7 @@ corner_radius_bottom_right = 8 [node name="PlanetServicesMenu" type="Control"] visible = false layout_mode = 0 -offset_right = 350.0 +offset_right = 320.0 offset_bottom = 250.0 mouse_filter = 2 script = ExtResource("1_services") @@ -34,18 +34,18 @@ anchor_bottom = 1.0 mouse_filter = 2 [node name="Panel" type="PanelContainer" parent="Center"] -custom_minimum_size = Vector2(350, 250) +custom_minimum_size = Vector2(320, 0) layout_mode = 2 theme_override_styles/panel = SubResource("PanelStyle") [node name="Contents" type="VBoxContainer" parent="Center/Panel"] layout_mode = 2 -theme_override_constants/separation = 12 +theme_override_constants/separation = 7 [node name="Title" type="Label" parent="Center/Panel/Contents"] layout_mode = 2 theme_override_colors/font_color = Color(0.47, 0.91, 0.74, 1) -theme_override_font_sizes/font_size = 17 +theme_override_font_sizes/font_size = 13 text = "USŁUGI TUBYLCÓW" horizontal_alignment = 1 diff --git a/scripts/captain/captain_skills.gd b/scripts/captain/captain_skills.gd index 1b67737..9ad3604 100644 --- a/scripts/captain/captain_skills.gd +++ b/scripts/captain/captain_skills.gd @@ -13,7 +13,7 @@ const DATA := { "tactical_maneuver": {"name": "Manewr taktyczny", "active": true, "values": [0.30, 0.60, 1.00], "color": "b9a0ef"}, "efficient_magazine": {"name": "Oszczędny magazynek", "active": false, "values": [0.10, 0.20, 0.50], "color": "a9d69a"}, "miner": {"name": "Górnik", "active": false, "values": [3, 6, 10], "color": "e5ac76"}, - "power_redirect": {"name": "Przekierowanie zasilania", "active": true, "values": [0.50, 0.90, 1.50], "color": "f28b6e"}, + "power_redirect": {"name": "Przekierowanie zasilania", "active": true, "values": [1.00, 1.80, 3.00], "color": "f28b6e"}, } static func value(skill: String, rank: int) -> float: @@ -22,7 +22,7 @@ static func value(skill: String, rank: int) -> float: return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1]) static func duration(skill: String) -> float: - return 15.0 if skill == "reactor_overload" else DURATION + return 15.0 if skill == "reactor_overload" else (7.0 if skill == "power_redirect" else DURATION) static func icon(skill: String) -> Texture2D: return load("res://assets/icons/captain/%s.png" % skill) as Texture2D @@ -47,5 +47,5 @@ static func description(skill: String, rank: int) -> String: "miner": return "+%d szt. surowca z każdej zniszczonej asteroidy." % roundi(value(skill, rank)) "power_redirect": - return "Regeneracja energii +%d%% przez 5 sekund. Zużywa wszystkie tarcze i blokuje ich odzysk na czas działania. Wymaga min. 25%% tarcz." % amount + return "Regeneracja energii +%d%% przez 7 sekund. Zużywa wszystkie tarcze i blokuje ich odzysk na czas działania. Wymaga min. 25%% tarcz." % amount return "" diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index b75cb3b..b5246bf 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -42,9 +42,18 @@ var _weapon_timer := 0.0 var _slow_remaining := 0.0 var _combat_target: Node2D var virus_controlled := false +var _virus_infection_attempted := false var _reward_claimed := false +func roll_virus_infection() -> bool: + if loot_tier == 2 or _virus_infection_attempted: + return false + _virus_infection_attempted = true + return randf() < 0.5 + func set_virus_controlled(value: bool) -> void: + if value and loot_tier == 2: + return if virus_controlled == value or is_queued_for_deletion(): return virus_controlled = value @@ -246,9 +255,21 @@ func select_combat_target() -> Node2D: closest_distance = distance if closest != null: return closest - if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")): + var player_visible := is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")) + var nearby_range := maxf(activation_range, weapon_range) + if player_visible and global_position.distance_to(_player.global_position) <= nearby_range: return _player - return null + # Infected ships become fallback targets only when the player is absent nearby. + for ally in get_tree().get_nodes_in_group("virus_allies"): + if ally.is_queued_for_deletion() or float(ally.get("hull")) <= 0.0: + continue + var distance := global_position.distance_to(ally.global_position) + if distance <= nearby_range and distance < closest_distance: + closest = ally + closest_distance = distance + if closest != null: + return closest + return _player if player_visible else null func apply_mine_hit() -> void: if is_queued_for_deletion() or hull <= 0.0: @@ -341,7 +362,7 @@ func get_shot_damage() -> float: func get_weapon_dps() -> float: var interval := get_fire_interval() - return get_shot_damage() / maxf(0.001, interval) * (1.0 + 0.25 * minf(3.0, interval) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0) + return get_shot_damage() / maxf(0.001, interval) * (1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, interval) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0) func _reward_captain() -> void: if is_instance_valid(_player): diff --git a/scripts/game/radiation_status.gd b/scripts/game/radiation_status.gd index 0ad2e63..12adb8e 100644 --- a/scripts/game/radiation_status.gd +++ b/scripts/game/radiation_status.gd @@ -1,5 +1,7 @@ extends Node +const DURATION := 10.0 + var remaining := 0.0 var tick_damage := 0.0 var credited_to_player := false @@ -16,7 +18,7 @@ static func apply(target: Node, damage_per_second: float, player_hit: bool) -> v status.set_script(load("res://scripts/game/radiation_status.gd")) status.name = "RadiationStatus" target.add_child(status) - status.remaining = 3.0 + status.remaining = DURATION status.tick_damage = maxf(status.tick_damage, damage_per_second) status.credited_to_player = player_hit diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 2dd1e96..694a599 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -199,6 +199,8 @@ func recycle_asteroid_fragment() -> void: _update_shield_visual() func try_control_enemy(enemy: CharacterBody2D) -> bool: + if not is_instance_valid(enemy) or int(enemy.get("loot_tier")) == 2: + return false var limit := int(get_passive_extension_value("virus")) if limit <= 0 or _is_destroyed: return false @@ -208,6 +210,8 @@ func try_control_enemy(enemy: CharacterBody2D) -> bool: controlled += 1 if controlled >= limit: return false + if not bool(enemy.call("roll_virus_infection")): + return false enemy.call("set_virus_controlled", true) return true diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index f2637f3..fbf4f70 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -24,7 +24,7 @@ func get_passive_description() -> String: var value := get_passive_value() match definition.passive_effect: "recycler": return "Każdy zebrany odłamek asteroidy odnawia %.1f%% maksymalnych tarcz i energii." % value - "virus": return "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. Wrogowie nie wybierają ich jako celu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value) + "virus": return "Ma 50%% szans na przejęcie uciekającego wroga (poniżej 10%% kadłuba). Jedna próba na statek; losowanie następuje, gdy jest wolne miejsce na przejęcie. Zamiast uciekać atakują swoich dawnych sojuszników. Bossowie są odporni na wirusa. Wrogowie atakują zawirusowane statki, gdy gracza nie ma w pobliżu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value) "titanium_armor": return "Pochłania 50%% otrzymywanych obrażeń, gdy kadłub ma nie więcej niż %d%% maksimum. Próg sprawdzany przed trafieniem." % int(value) return "" diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 88581b8..a79a8ab 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -446,6 +446,6 @@ func get_total_dps() -> float: for item in weapon_slots: var value := _weapon_dps(item, bonuses) if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: - value *= 1.0 + 0.25 * minf(3.0, item.definition.fire_interval) + value *= 1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, item.definition.fire_interval) total += value return total diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 3e11cd6..4639c61 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -2,6 +2,8 @@ extends Control signal next_level_requested +const BUTTON_STYLE := preload("res://scripts/ui/hud_button_style.gd") +const CREDIT_ICON := preload("res://assets/icons/resources/credits.svg") @onready var next_button: Button = $Center/Panel/Contents/NextButton @onready var buy_ammunition_button: Button = $Center/Panel/Contents/BuyAmmunitionButton @onready var reset_skills_button: Button = $Center/Panel/Contents/ResetSkillsButton @@ -13,32 +15,30 @@ var _inventory: ShipInventory var planet: Node2D var _open_tween: Tween var _missile_button: Button - var _gamma_buy: Button +var _stock_labels: Dictionary = {} func _ready() -> void: - get_parent().get_node("FloatingWindows").call("register_window", self, panel, $Center/Panel/Contents/Title) _gamma_buy = Button.new() _gamma_buy.name = "BuyIrradiatedAmmunition" - _gamma_buy.custom_minimum_size.y = 36 - _gamma_buy.add_theme_font_size_override("font_size", 12) - _gamma_buy.focus_mode = Control.FOCUS_NONE - $Center/Panel/Contents.add_child(_gamma_buy) - _gamma_buy.pressed.connect(func() -> void: _inventory.buy_irradiated_ammunition()) + _missile_button = Button.new() + _missile_button.name = "BuyMissile" + var contents := $Center/Panel/Contents + contents.add_child(_gamma_buy) + contents.add_child(_missile_button) + reset_skills_button.hide() + _build_compact_shop() + get_parent().get_node("FloatingWindows").call("register_window", self, panel, contents.get_node("Title")) next_button.pressed.connect(func() -> void: next_level_requested.emit()) buy_ammunition_button.pressed.connect(_buy_ammunition) - reset_skills_button.hide() - _missile_button = Button.new() - _missile_button.custom_minimum_size.y = 36 - _missile_button.add_theme_font_size_override("font_size", 12) - _missile_button.focus_mode = Control.FOCUS_NONE - _missile_button.tooltip_text = "Jedna sztuka amunicji rakietowej do dowolnej wyrzutni." + _gamma_buy.pressed.connect(func() -> void: + if visible and _inventory: + _inventory.buy_irradiated_ammunition() + ) _missile_button.pressed.connect(func() -> void: if visible and _inventory: _inventory.buy_missile() ) - $Center/Panel/Contents.add_child(_missile_button) - $Center/Panel/Contents.move_child(_missile_button, reset_skills_button.get_index()) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.ammunition_changed.connect(_refresh_ammunition_service) @@ -46,11 +46,100 @@ func _ready() -> void: GameState.captain.changed.connect(_refresh_ammunition_service) _refresh_ammunition_service() +func _build_compact_shop() -> void: + var contents := $Center/Panel/Contents + contents.add_theme_constant_override("separation", 7) + status_label.add_theme_font_size_override("font_size", 10) + status_label.add_theme_color_override("font_color", Color("86b7aa")) + services_label.add_theme_font_size_override("font_size", 11) + services_label.add_theme_color_override("font_color", Color("efc982")) + services_label.size_flags_vertical = Control.SIZE_FILL + var wallet := HBoxContainer.new() + wallet.name = "Wallet" + wallet.alignment = BoxContainer.ALIGNMENT_CENTER + wallet.add_theme_constant_override("separation", 6) + contents.add_child(wallet) + contents.move_child(wallet, 2) + var money_icon := TextureRect.new() + money_icon.texture = CREDIT_ICON + money_icon.custom_minimum_size = Vector2(15, 15) + money_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + money_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + money_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE + wallet.add_child(money_icon) + services_label.reparent(wallet) + var offers := HBoxContainer.new() + offers.name = "Offers" + offers.add_theme_constant_override("separation", 6) + contents.add_child(offers) + contents.move_child(offers, 3) + _add_offer(offers, buy_ammunition_button, "ammunition", "Zwykła", 100, Color("e8b46f")) + _add_offer(offers, _gamma_buy, "irradiated_ammunition", "Radiacyjna", 10, Color("add580")) + _add_offer(offers, _missile_button, "missile", "Rakieta", 1, Color("82cddd")) + BUTTON_STYLE.apply(next_button, Color("78d7be")) + next_button.custom_minimum_size = Vector2(0, 34) + next_button.add_theme_font_size_override("font_size", 11) + next_button.icon = preload("res://assets/icons/extensions/teleport_art.png") + next_button.expand_icon = true + next_button.add_theme_constant_override("icon_max_width", 21) + contents.move_child(next_button, contents.get_child_count() - 1) + +func _add_offer(offers: HBoxContainer, button: Button, item_id: String, caption: String, amount: int, accent: Color) -> void: + var card := PanelContainer.new() + card.name = item_id.capitalize().replace(" ", "") + "Offer" + card.custom_minimum_size.x = 94 + card.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var style := StyleBoxFlat.new() + style.bg_color = Color("101e2a") + style.border_color = accent.darkened(0.65) + style.set_border_width_all(1) + style.border_width_top = 2 + style.set_corner_radius_all(6) + style.set_content_margin_all(6) + card.add_theme_stylebox_override("panel", style) + offers.add_child(card) + var column := VBoxContainer.new() + column.add_theme_constant_override("separation", 4) + card.add_child(column) + var icon := TextureRect.new() + icon.texture = load("res://assets/icons/resources/%s.png" % item_id) + icon.custom_minimum_size = Vector2(36, 36) + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.mouse_filter = Control.MOUSE_FILTER_IGNORE + column.add_child(icon) + var title := Label.new() + title.text = "%s ×%d" % [caption, amount] + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.add_theme_font_size_override("font_size", 10) + title.add_theme_color_override("font_color", accent.lightened(0.2)) + column.add_child(title) + var stock := Label.new() + stock.name = "Stock" + stock.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + stock.add_theme_font_size_override("font_size", 9) + stock.add_theme_color_override("font_color", Color("8ba4b7")) + column.add_child(stock) + _stock_labels[item_id] = stock + button.reparent(column) + BUTTON_STYLE.apply(button, accent) + button.custom_minimum_size = Vector2(0, 28) + button.add_theme_font_size_override("font_size", 10) + button.icon = CREDIT_ICON + button.expand_icon = true + button.add_theme_constant_override("icon_max_width", 12) + for state in ["normal", "hover", "pressed", "disabled"]: + var frame := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat + frame.set_content_margin_all(4) + frame.set_corner_radius_all(4) + frame.shadow_size = 0 + button.add_theme_stylebox_override(state, frame) + func show_services(at_level_cap: bool, portal_open: bool = false) -> void: _refresh_ammunition_service() next_button.disabled = at_level_cap or portal_open - next_button.text = "OSIĄGNIĘTO OSTATNI UKŁAD" if at_level_cap else ("WORMHOLE OTWARTY" if portal_open else "IDŹ DO NASTĘPNEGO POZIOMU") - status_label.text = "Wleć w wormhole, aby przejść dalej." if portal_open else "Układ bezpieczny. Usługi dostępne." + next_button.text = "OSTATNI UKŁAD" if at_level_cap else ("WORMHOLE OTWARTY" if portal_open else "OTWÓRZ WORMHOLE") + status_label.text = "Wleć w wormhole, aby przejść dalej." if portal_open else "Bezpieczny układ · zaopatrzenie" if visible: return show() @@ -81,30 +170,31 @@ func _position_at_planet() -> void: size = panel.get_combined_minimum_size() var viewport_size := get_viewport_rect().size var sidebar := get_parent().get_node_or_null("ShipDrawer") as Control - var screen_right := viewport_size.x - (sidebar.size.x + 8.0 if sidebar != null and sidebar.visible else 8.0) + var screen_right := minf(viewport_size.x - 8.0, sidebar.get_global_rect().position.x - 8.0) if sidebar != null and sidebar.visible else viewport_size.x - 8.0 var target := planet_screen + Vector2(planet_radius + 24.0, -size.y * 0.5) if target.x + size.x > screen_right: target.x = planet_screen.x - planet_radius - 24.0 - size.x position = Vector2(clampf(target.x, 8.0, maxf(8.0, screen_right - size.x)), clampf(target.y, 8.0, maxf(8.0, viewport_size.y - size.y - 150.0))) func _refresh_ammunition_service() -> void: - if _gamma_buy != null: - _gamma_buy.text = "KUP 10 NAPROMIENIOWANEJ — %d KREDYTÓW" % (_inventory.get_irradiated_ammunition_buy_cost() if _inventory else ShipInventory.IRRADIATED_AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level)) - _gamma_buy.disabled = _inventory == null or not _inventory.can_buy_irradiated_ammunition() - _missile_button.disabled = _inventory == null or not _inventory.can_buy_missile() - _missile_button.text = "KUP +1 RAKIETĘ — %d KREDYTÓW" % (_inventory.get_missile_buy_cost() if _inventory else ShipInventory.MISSILE_BUY_BASE_COST) - reset_skills_button.disabled = not GameState.captain.can_reset_skills() - reset_skills_button.text = "RESET SKILLI — %d KREDYTÓW" % GameState.captain.get_skill_reset_cost() - reset_skills_button.tooltip_text = "Zwraca wszystkie %d wydanych punktów nauki. Koszt: 100 kredytów × poziom kapitana." % GameState.captain.get_spent_learning_points() + var ordinary_cost := _inventory.get_ammunition_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level)) + var gamma_cost := _inventory.get_irradiated_ammunition_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.IRRADIATED_AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level)) + var missile_cost := _inventory.get_missile_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level)) + buy_ammunition_button.text = "KUP %d" % ordinary_cost + _gamma_buy.text = "KUP %d" % gamma_cost + _missile_button.text = "KUP %d" % missile_cost buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition() - if _inventory: - buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()] - services_label.text = "Amunicja: %d • Kredyty: %d\nRekin biznesu: −%d%% ceny zakupów" % [_inventory.ammunition, GameState.captain.credits, roundi(GameState.captain.get_skill_bonus("business_shark") * 100)] + _gamma_buy.disabled = _inventory == null or not _inventory.can_buy_irradiated_ammunition() + _missile_button.disabled = _inventory == null or not _inventory.can_buy_missile() + buy_ammunition_button.tooltip_text = "Kup 100 zwykłych pocisków za %d kredytów." % ordinary_cost + _gamma_buy.tooltip_text = "Kup 10 pocisków radiacyjnych za %d kredytów." % gamma_cost + _missile_button.tooltip_text = "Kup 1 rakietę za %d kredytów." % missile_cost + var discount := roundi(GameState.captain.get_skill_bonus("business_shark") * 100) + services_label.text = "%d kredytów%s" % [GameState.captain.credits, " · rabat %d%%" % discount if discount > 0 else ""] + _stock_labels["ammunition"].text = "Zapas: %d" % (_inventory.ammunition if _inventory else 0) + _stock_labels["irradiated_ammunition"].text = "Zapas: %d" % (_inventory.irradiated_ammunition if _inventory else 0) + _stock_labels["missile"].text = "Zapas: %d" % (_inventory.missile_ammunition if _inventory else 0) func _buy_ammunition() -> void: if visible and _inventory: _inventory.buy_ammunition() - -func _reset_skills() -> void: - if visible: - GameState.captain.reset_skills() diff --git a/tests/abilities/passive_extensions_test.gd b/tests/abilities/passive_extensions_test.gd index 9e89ca1..9263ae5 100644 --- a/tests/abilities/passive_extensions_test.gd +++ b/tests/abilities/passive_extensions_test.gd @@ -27,6 +27,14 @@ func enemy_at(point: Vector2) -> CharacterBody2D: enemy.set("_weapon_timer", 100.0) return enemy +func seed_successful_infection() -> void: + for value in 100: + seed(value) + if randf() < 0.5: + seed(value) + return + assert(false) + func _ready() -> void: await get_tree().process_frame GameState.start_new_run() @@ -108,6 +116,7 @@ func _ready() -> void: var third := enemy_at(Vector2(450, 0)) first.hull = 1 second.hull = 1 + seed_successful_infection() first.call("_physics_process", 0.01) second.call("_physics_process", 0.01) assert(first.virus_controlled and not second.virus_controlled) @@ -135,6 +144,7 @@ func _ready() -> void: assert(ship.shields == player_shields) # Increased capacity takes another ship; downgrade and unequip release extras. virus.passive_value = 2 + seed_successful_infection() second.call("_physics_process", 0.01) assert(second.virus_controlled) virus.passive_value = 1 diff --git a/tests/abilities/virus_chance_test.gd b/tests/abilities/virus_chance_test.gd new file mode 100644 index 0000000..cfbe427 --- /dev/null +++ b/tests/abilities/virus_chance_test.gd @@ -0,0 +1,23 @@ +extends Node +func _ready() -> void: + var script := preload("res://scripts/game/enemy_ship.gd") + seed(20261017) + var successes := 0 + var failures := 0 + for index in 2000: + var enemy := script.new() + var won: bool = enemy.roll_virus_infection() + successes += int(won) + failures += int(not won) + assert(not enemy.roll_virus_infection(), "A second frame must never reroll infection") + assert(not enemy.roll_virus_infection(), "Failed rolls must remain failed") + enemy.free() + assert(successes >= 900 and successes <= 1100) + assert(failures > 0 and successes > 0) + var boss := script.new() + boss.loot_tier = 2 + for index in 100: + assert(not boss.roll_virus_infection()) + boss.free() + print("VIRUS_CHANCE_TEST: PASS ", successes, "/2000 first attempts, no rerolls, bosses immune") + get_tree().quit() diff --git a/tests/abilities/virus_chance_test.gd.uid b/tests/abilities/virus_chance_test.gd.uid new file mode 100644 index 0000000..09dbd81 --- /dev/null +++ b/tests/abilities/virus_chance_test.gd.uid @@ -0,0 +1 @@ +uid://dt8b1tr1i5dfu diff --git a/tests/abilities/virus_chance_test.tscn b/tests/abilities/virus_chance_test.tscn new file mode 100644 index 0000000..1f8acca --- /dev/null +++ b/tests/abilities/virus_chance_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/abilities/virus_chance_test.gd" id="1"] +[node name="VirusChanceTest" type="Node"] +script = ExtResource("1") \ No newline at end of file diff --git a/tests/abilities/virus_player_damage_test.gd b/tests/abilities/virus_player_damage_test.gd index 2bb9eeb..ed16f64 100644 --- a/tests/abilities/virus_player_damage_test.gd +++ b/tests/abilities/virus_player_damage_test.gd @@ -25,6 +25,17 @@ func _ready() -> void: infected.shields = infected.max_shields assert(hostile.select_combat_target() == ship) assert(infected.select_combat_target() == hostile) + ship.position = Vector2(-2000, 0) + assert(hostile.select_combat_target() == infected) + ship.position = Vector2.ZERO + assert(hostile.select_combat_target() == ship) + var boss := make_enemy(Vector2(4000, 0)) + boss.apply_level_settings(1, 2) + boss.hull = 1.0 + assert(not ship.try_control_enemy(boss)) + boss.set_virus_controlled(true) + assert(not boss.virus_controlled and boss.is_in_group("enemies")) + boss.queue_free() var xp_after_conversion := GameState.captain.experience await get_tree().physics_frame await get_tree().physics_frame diff --git a/tests/captain/reactor_duration_test.gd b/tests/captain/reactor_duration_test.gd index f107a5a..05714a9 100644 --- a/tests/captain/reactor_duration_test.gd +++ b/tests/captain/reactor_duration_test.gd @@ -19,5 +19,21 @@ func _ready() -> void: captain.tick_skills(0.11) assert(not captain.is_skill_active("reactor_overload")) assert(preload("res://scripts/captain/captain_skills.gd").description("reactor_overload", 1).contains("15 sekund")) - print("REACTOR_DURATION_TEST: PASS 15 seconds, 30-second cooldown, other skills still 5 seconds") + for rank in range(1, 4): + captain.skill_cooldowns.clear() + captain.skills["power_redirect"] = rank + assert(captain.equip_skill(2, "power_redirect")) + ship.shields = ship.max_shields + assert(captain.activate_skill("power_redirect", ship)) + assert(ship.shields == 0.0) + assert(captain.skill_durations.power_redirect == 7.0) + assert(captain.skill_cooldowns.power_redirect == 30.0) + assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [1.0, 1.8, 3.0][rank - 1])) + captain.tick_skills(6.99) + assert(captain.is_skill_active("power_redirect")) + captain.tick_skills(0.02) + assert(not captain.is_skill_active("power_redirect")) + assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0)) + assert(preload("res://scripts/captain/captain_skills.gd").description("power_redirect", 3).contains("7 sekund")) + print("REACTOR_DURATION_TEST: PASS overload 15s, redirection 7s at +100/180/300%, 30s cooldown, tactical maneuver 5s") get_tree().quit() diff --git a/tests/captain/reactor_duration_test.gd.uid b/tests/captain/reactor_duration_test.gd.uid new file mode 100644 index 0000000..8727f0a --- /dev/null +++ b/tests/captain/reactor_duration_test.gd.uid @@ -0,0 +1 @@ +uid://bsqjxui01uk7c diff --git a/tests/items/damage_types_test.gd b/tests/items/damage_types_test.gd index 25c13b1..1c0203f 100644 --- a/tests/items/damage_types_test.gd +++ b/tests/items/damage_types_test.gd @@ -69,9 +69,9 @@ func _ready() -> void: assert(is_equal_approx(enemy.hull, 76)) var status: Node = enemy.get_node("RadiationStatus") status.set_physics_process(false) - for index in 6: + for index in 20: status._physics_process(0.5) - assert(is_equal_approx(enemy.hull, 70)) + assert(is_equal_approx(enemy.hull, 56)) assert(is_equal_approx(status.remaining, 0)) assert(enemy.get_node("Sprite2D").modulate == Color.WHITE) for kind in 5: diff --git a/tests/items/radiation_duration_test.gd b/tests/items/radiation_duration_test.gd new file mode 100644 index 0000000..9d4efa2 --- /dev/null +++ b/tests/items/radiation_duration_test.gd @@ -0,0 +1,30 @@ +extends Node +const RADIATION = preload("res://scripts/game/radiation_status.gd") +class Target: + extends CharacterBody2D + var total_damage := 0.0 + func take_damage(amount: float, _critical: bool, _kind: int) -> void: + total_damage += amount + func take_weapon_hit(amount: float, critical: bool, kind: int) -> void: + take_damage(amount, critical, kind) + +func _ready() -> void: + var target := Target.new() + add_child(target) + RADIATION.apply(target, 2.0, true) + var status := target.get_node("RadiationStatus") + status.set_physics_process(false) + assert(status.remaining == 10.0) + for index in 6: + status._physics_process(0.5) + assert(status.remaining == 7.0 and target.total_damage == 6.0) + RADIATION.apply(target, 1.0, true) + assert(status.remaining == 10.0 and status.tick_damage == 2.0) + for index in 20: + status._physics_process(0.5) + assert(status.remaining == 0.0 and target.total_damage == 26.0) + status._physics_process(1.0) + assert(target.total_damage == 26.0) + assert(preload("res://resources/items/gamma_cannon.tres").description.contains("10 s")) + print("RADIATION_DURATION_TEST: PASS 10s damage, refreshed timer, unchanged per-second damage, expiry and description") + get_tree().quit() diff --git a/tests/items/radiation_duration_test.gd.uid b/tests/items/radiation_duration_test.gd.uid new file mode 100644 index 0000000..bf69f8b --- /dev/null +++ b/tests/items/radiation_duration_test.gd.uid @@ -0,0 +1 @@ +uid://rvxal1pmjjel diff --git a/tests/items/radiation_duration_test.tscn b/tests/items/radiation_duration_test.tscn new file mode 100644 index 0000000..262dea1 --- /dev/null +++ b/tests/items/radiation_duration_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/radiation_duration_test.gd" id="1"] +[node name="RadiationDurationTest" type="Node"] +script = ExtResource("1") \ No newline at end of file diff --git a/tests/previews/planet_services_preview.gd b/tests/previews/planet_services_preview.gd new file mode 100644 index 0000000..7f4751c --- /dev/null +++ b/tests/previews/planet_services_preview.gd @@ -0,0 +1,44 @@ +extends "res://scripts/game/game.gd" + +func _ready() -> void: + super._ready() + set_physics_process(false) + 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) + player.position = protected_planet.position + Vector2(float(protected_planet.get("radius")) + 110.0, 0) + player.reset_physics_interpolation() + GameState.captain.credits = 2500 + GameState.captain.skills["business_shark"] = 2 + var inventory := player.get_node("ShipInventory") as ShipInventory + inventory.ammunition = 240 + inventory.irradiated_ammunition = 12 + inventory.missile_ammunition = 7 + services_menu.show_services(false) + await get_tree().create_timer(0.5).timeout + assert(services_menu.get("panel").size.x <= 340.0) + assert(services_menu.get("panel").size.y <= 290.0) + if "--capture-preview" in OS.get_cmdline_user_args(): + await RenderingServer.frame_post_draw + get_viewport().get_texture().get_image().save_png("res://artifacts/planet_services_preview.png") + print("PLANET_SERVICES_PREVIEW: captured ", services_menu.get("panel").size) + var credits := GameState.captain.credits + var cost := inventory.get_ammunition_buy_cost() + services_menu.get("buy_ammunition_button").pressed.emit() + assert(inventory.ammunition == 340 and GameState.captain.credits == credits - cost) + credits = GameState.captain.credits + cost = inventory.get_irradiated_ammunition_buy_cost() + services_menu.get("_gamma_buy").pressed.emit() + assert(inventory.irradiated_ammunition == 22 and GameState.captain.credits == credits - cost) + credits = GameState.captain.credits + cost = inventory.get_missile_buy_cost() + services_menu.get("_missile_button").pressed.emit() + assert(inventory.missile_ammunition == 8 and GameState.captain.credits == credits - cost) + GameState.captain.credits = 0 + GameState.captain.emit_changed() + assert(services_menu.get("buy_ammunition_button").disabled and services_menu.get("_gamma_buy").disabled and services_menu.get("_missile_button").disabled) + services_menu.show_services(false, true) + assert(services_menu.get("next_button").disabled) + print("PLANET_SERVICES_TEST: PASS compact dimensions, all purchases, discounts, stock refresh, insufficient credits and portal state") + get_tree().quit() diff --git a/tests/previews/planet_services_preview.gd.uid b/tests/previews/planet_services_preview.gd.uid new file mode 100644 index 0000000..142bddb --- /dev/null +++ b/tests/previews/planet_services_preview.gd.uid @@ -0,0 +1 @@ +uid://cmfun1xppkec8 diff --git a/tests/previews/planet_services_preview.tscn b/tests/previews/planet_services_preview.tscn new file mode 100644 index 0000000..f6d22ed --- /dev/null +++ b/tests/previews/planet_services_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/planet_services_preview.gd" id="2"] +[node name="PlanetServicesPreview" instance=ExtResource("1")] +script = ExtResource("2") \ No newline at end of file