From b5338a7564199db9c0727ec7fd8741b71e9c85c8 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sun, 11 Oct 2026 18:42:15 +0200 Subject: [PATCH] Zmiany --- resources/items/affix_database.tres | 6 +- resources/items/affixes/shield_on_damage.tres | 14 +++ resources/items/plasma_cannon.tres | 8 +- scenes/ui/ship_equipment_panel.tscn | 12 +-- scenes/ui/ship_inventory_panel.tscn | 2 +- scripts/captain/captain_progress.gd | 4 +- scripts/game/enemy_ship.gd | 16 ++- scripts/game/flight_hud.gd | 2 + scripts/game/ship_controller.gd | 8 ++ scripts/game/ship_energy_indicator.gd | 30 ++++-- scripts/items/credit_pickup.gd | 36 +++++++ scripts/items/credit_pickup.gd.uid | 1 + scripts/items/item_instance.gd | 2 +- scripts/items/ship_inventory.gd | 4 +- tests/captain/captain_system_test.gd | 11 +++ tests/items/credit_pickup_test.gd | 51 ++++++++++ tests/items/credit_pickup_test.gd.uid | 1 + tests/items/credit_pickup_test.tscn | 4 + .../items/machine_gun_shield_recovery_test.gd | 99 +++++++++++++++++++ .../machine_gun_shield_recovery_test.tscn | 4 + tests/items/shield_on_damage_test.gd | 76 ++++++++++++++ tests/items/shield_on_damage_test.gd.uid | 1 + tests/items/shield_on_damage_test.tscn | 4 + tests/items/weapon_damage_test.gd | 2 +- 24 files changed, 371 insertions(+), 27 deletions(-) create mode 100644 resources/items/affixes/shield_on_damage.tres create mode 100644 scripts/items/credit_pickup.gd create mode 100644 scripts/items/credit_pickup.gd.uid create mode 100644 tests/items/credit_pickup_test.gd create mode 100644 tests/items/credit_pickup_test.gd.uid create mode 100644 tests/items/credit_pickup_test.tscn create mode 100644 tests/items/machine_gun_shield_recovery_test.gd create mode 100644 tests/items/machine_gun_shield_recovery_test.tscn create mode 100644 tests/items/shield_on_damage_test.gd create mode 100644 tests/items/shield_on_damage_test.gd.uid create mode 100644 tests/items/shield_on_damage_test.tscn diff --git a/resources/items/affix_database.tres b/resources/items/affix_database.tres index 3c1ed73..a4dbd04 100644 --- a/resources/items/affix_database.tres +++ b/resources/items/affix_database.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=14 format=3] +[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=15 format=3] [ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"] [ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"] @@ -15,6 +15,8 @@ [ext_resource type="Resource" path="res://resources/items/affixes/missile_cooldown.tres" id="13_affix"] +[ext_resource type="Resource" path="res://resources/items/affixes/shield_on_damage.tres" id="14_affix"] + [resource] script = ExtResource("1_database") -affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix")]) +affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix")]) diff --git a/resources/items/affixes/shield_on_damage.tres b/resources/items/affixes/shield_on_damage.tres new file mode 100644 index 0000000..b461031 --- /dev/null +++ b/resources/items/affixes/shield_on_damage.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"] +[resource] +script = ExtResource("1_affix") +affix_id = "shield_on_damage" +display_name = "Odzysk tarcz z obrażeń" +description = "Globalnie odnawia tarcze o procent faktycznie zadanych obrażeń każdą bronią." +stat_key = "shield_on_damage_percent" +unit = "%" +category_mask = 63 +minimum_value_level_1 = 1.0 +maximum_value_level_1 = 1.0 +minimum_value_level_1000 = 5.0 +maximum_value_level_1000 = 5.0 diff --git a/resources/items/plasma_cannon.tres b/resources/items/plasma_cannon.tres index 89e1c3d..bf5ed46 100644 --- a/resources/items/plasma_cannon.tres +++ b/resources/items/plasma_cannon.tres @@ -9,14 +9,14 @@ display_name = "Działko plazmowe" category = 0 description = "Szybkostrzelna broń krótkiego zasięgu." primary_stat = "damage" -primary_value = 15.0 +primary_value = 12.0 stat_suffix = " DMG" weapon_kind = 0 fire_interval = 0.2857 projectile_speed = 720.0 weapon_range = 350.0 energy_cost = 4.0 -damage_lower_roll_min = 10 -damage_lower_roll_max = 14 +damage_lower_roll_min = 8 +damage_lower_roll_max = 11 damage_spread_roll_min = 2 -damage_spread_roll_max = 4 +damage_spread_roll_max = 3 diff --git a/scenes/ui/ship_equipment_panel.tscn b/scenes/ui/ship_equipment_panel.tscn index ba58e17..d4d7874 100644 --- a/scenes/ui/ship_equipment_panel.tscn +++ b/scenes/ui/ship_equipment_panel.tscn @@ -59,9 +59,9 @@ script = ExtResource("1_schematic") [node name="Weapon1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] layout_mode = 0 -offset_left = 44.0 +offset_left = 0.0 offset_top = 22.0 -offset_right = 134.0 +offset_right = 90.0 offset_bottom = 66.0 slot_kind = "weapon" slot_index = 0 @@ -69,9 +69,9 @@ overlay_slot = true [node name="Weapon2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] layout_mode = 0 -offset_left = 172.0 +offset_left = 216.0 offset_top = 22.0 -offset_right = 262.0 +offset_right = 306.0 offset_bottom = 66.0 slot_kind = "weapon" slot_index = 1 @@ -80,9 +80,9 @@ overlay_slot = true [node name="Missile" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] layout_mode = 0 offset_left = 108.0 -offset_top = 260.0 +offset_top = 22.0 offset_right = 198.0 -offset_bottom = 304.0 +offset_bottom = 66.0 slot_kind = "weapon" slot_index = 2 overlay_slot = true diff --git a/scenes/ui/ship_inventory_panel.tscn b/scenes/ui/ship_inventory_panel.tscn index 67f8ba8..2a6917a 100644 --- a/scenes/ui/ship_inventory_panel.tscn +++ b/scenes/ui/ship_inventory_panel.tscn @@ -57,7 +57,7 @@ theme_override_constants/separation = 12 layout_mode = 2 size_flags_horizontal = 3 theme_override_font_sizes/font_size = 11 -text = "AMUNICJA: 300" +text = "AMUNICJA: 50" [node name="CraftButton" type="Button" parent="Contents/Ammunition"] layout_mode = 2 diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index da59f4d..915e607 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -75,10 +75,10 @@ func add_rewards(xp_amount: int, credit_amount: int) -> void: learning_points += BALANCE.LEARNING_POINTS_PER_LEVEL emit_changed() -func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float) -> Vector2i: +func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i: var index := clampi(tier, 0, 2) var scale_factor := 1.0 + 0.05 * (clampi(system_level, 1, 1000) - 1) var xp := roundi(BALANCE.KILL_XP[index] * scale_factor * maxf(0.0, xp_multiplier)) var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier)) - add_rewards(xp, money) + add_rewards(xp, money if grant_credits else 0) return Vector2i(xp, money) diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 36e2ba8..0f79fe5 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -6,6 +6,7 @@ const PLASMA_PROJECTILE_SCENE := preload("res://scenes/game/enemy_plasma_project const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd") const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd") const LOOT_PICKUP := preload("res://scripts/items/loot_pickup.gd") +const LOOT_OFFSET := Vector2(0, 36) const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd") @export_enum("Normal", "Elite", "Boss") var loot_tier: int = 0 @@ -99,7 +100,12 @@ func _fire_plasma(direction: Vector2) -> void: WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, ItemDefinition.WeaponKind.PLASMA, -4.0) func take_weapon_hit(amount: float, critical: bool) -> void: + var actual_damage := minf(maxf(0.0, amount), maxf(0.0, hull)) + if is_queued_for_deletion(): + actual_damage = 0.0 take_damage(amount, critical) + if is_instance_valid(_player): + _player.call("restore_shields_from_damage", actual_damage) func take_damage(amount: float, critical: bool = false) -> void: if is_queued_for_deletion() or hull <= 0.0: @@ -114,7 +120,13 @@ func take_damage(amount: float, critical: bool = false) -> void: func _reward_captain() -> void: if is_instance_valid(_player): - GameState.captain.reward_enemy(GameState.system_level, loot_tier, float(_player.get("experience_multiplier")), float(_player.get("credits_multiplier"))) + var reward := GameState.captain.reward_enemy(GameState.system_level, loot_tier, float(_player.get("experience_multiplier")), float(_player.get("credits_multiplier")), false) + if reward.y > 0: + var credits := Node2D.new() + credits.set_script(preload("res://scripts/items/credit_pickup.gd")) + credits.set("amount", reward.y) + credits.position = position - LOOT_OFFSET + get_parent().call_deferred("add_child", credits) func _drop_loot() -> void: var chance := 0.65 if loot_tier == 0 else (0.9 if loot_tier == 1 else 1.0) @@ -123,7 +135,7 @@ func _drop_loot() -> void: var pickup := Node2D.new() pickup.set_script(LOOT_PICKUP) pickup.set("item", ITEM_GENERATOR.generate(GameState.system_level, loot_tier)) - pickup.position = position + pickup.position = position + LOOT_OFFSET get_parent().call_deferred("add_child", pickup) func _spawn_destruction_effect() -> void: diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index d1e3006..9d2e5be 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -133,6 +133,7 @@ Redukcja obrażeń: %.1f%% Regeneracja energii: %.1f pkt/s (%+.1f) Ładowanie solarne: +%.1f pkt/s Regeneracja tarcz: %.1f pkt/s (%+.1f) +Odzysk tarcz z obrażeń: %.1f%% Maks. kadłub: %.1f (%+.1f pkt) Maks. tarcze: %.1f (%+.1f pkt) Maks. energia: %.1f (%+.1f pkt) @@ -151,6 +152,7 @@ Zdobywane kredyty: %+.1f%%""" % [ float(player.call("get_solar_regeneration")), float(player.get("shield_regeneration")), float(player.get("shield_regeneration")) - float(player.get("base_shield_regeneration")), + float(player.get("shield_on_damage_percent")), float(player.get("max_hull")), float(player.get("max_hull")) - float(player.get("base_max_hull")), float(player.get("max_shields")), float(player.get("max_shields")) - float(player.get("base_max_shields")), float(player.get("max_energy")), float(player.get("max_energy")) - float(player.get("base_max_energy")), diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 0bf308c..76d576a 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -36,6 +36,7 @@ var shield_regeneration := 0.0 var damage_multiplier := 1.0 var experience_multiplier := 1.0 var credits_multiplier := 1.0 +var shield_on_damage_percent := 0.0 var critical_chance_bonus := 0.0 var critical_damage_bonus := 0.0 @export var solar_charge_radius := 550.0 @@ -73,6 +74,7 @@ func apply_equipment_bonuses(bonuses: Dictionary) -> void: damage_multiplier = 1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0 experience_multiplier = 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0 credits_multiplier = 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0 + shield_on_damage_percent = maxf(0.0, float(bonuses.get("shield_on_damage_percent", 0.0))) critical_chance_bonus = float(bonuses.get("critical_chance_percent", 0.0)) / 100.0 critical_damage_bonus = float(bonuses.get("critical_damage_percent", 0.0)) / 100.0 energy_regeneration = base_energy_regeneration + float(bonuses.get("energy_regeneration", 0.0)) @@ -148,6 +150,12 @@ func take_damage(amount: float) -> void: if hull <= 0.0: _destroy_ship() +func restore_shields_from_damage(actual_damage: float) -> void: + if _is_destroyed or actual_damage <= 0.0 or shield_on_damage_percent <= 0.0: + return + shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0) + _update_shield_visual() + func get_evasion_chance() -> float: return GameState.captain.get_skill_bonus("piloting") diff --git a/scripts/game/ship_energy_indicator.gd b/scripts/game/ship_energy_indicator.gd index c4744d1..054eb3c 100644 --- a/scripts/game/ship_energy_indicator.gd +++ b/scripts/game/ship_energy_indicator.gd @@ -1,8 +1,9 @@ extends Node2D -const FONT := preload("res://assets/fonts/font2.ttf") var player: CharacterBody2D var _display_ratio := 1.0 +var _display_shield_ratio := 1.0 +var _display_hull_ratio := 1.0 var _background: StyleBoxFlat func _ready() -> void: @@ -13,6 +14,8 @@ func _ready() -> void: reset_physics_interpolation() z_index = 24 _display_ratio = _energy_ratio() + _display_shield_ratio = _shield_ratio() + _display_hull_ratio = _hull_ratio() _background = StyleBoxFlat.new() _background.bg_color = Color("101925") _background.border_color = Color("456277") @@ -33,11 +36,25 @@ func _process(delta: float) -> void: _display_ratio = lerpf(_display_ratio, target, 1.0 - exp(-20.0 * delta)) if absf(_display_ratio - target) < 0.001: _display_ratio = target + var shield_target := _shield_ratio() + _display_shield_ratio = lerpf(_display_shield_ratio, shield_target, 1.0 - exp(-20.0 * delta)) + if absf(_display_shield_ratio - shield_target) < 0.001: + _display_shield_ratio = shield_target + var hull_target := _hull_ratio() + _display_hull_ratio = lerpf(_display_hull_ratio, hull_target, 1.0 - exp(-20.0 * delta)) + if absf(_display_hull_ratio - hull_target) < 0.001: + _display_hull_ratio = hull_target queue_redraw() func _energy_ratio() -> float: return clampf(float(player.get("energy")) / maxf(1.0, float(player.get("max_energy"))), 0.0, 1.0) +func _shield_ratio() -> float: + return clampf(float(player.get("shields")) / maxf(1.0, float(player.get("max_shields"))), 0.0, 1.0) + +func _hull_ratio() -> float: + return clampf(float(player.get("hull")) / maxf(1.0, float(player.get("max_hull"))), 0.0, 1.0) + func _draw() -> void: if not is_instance_valid(player): return @@ -45,8 +62,9 @@ func _draw() -> void: var bar_position := Vector2(-44, 48) draw_style_box(_background, Rect2(bar_position - Vector2(2, 2), Vector2(92, 10))) draw_rect(Rect2(bar_position, Vector2(88 * _display_ratio, 6)), color) - var charging := float(player.call("get_solar_regeneration")) > 0.0 and _energy_ratio() < 1.0 - var caption := "ENERGIA %d%%" % roundi(_energy_ratio() * 100) - draw_string(FONT, bar_position + Vector2(0, 22), caption, HORIZONTAL_ALIGNMENT_CENTER, 88, 10, color) - if charging: - draw_string(FONT, bar_position + Vector2(-18, 36), "ŁADOWANIE SOLARNE", HORIZONTAL_ALIGNMENT_CENTER, 124, 9, Color("71e3ff")) + var shield_bar_position := bar_position + Vector2(0, 14) + draw_style_box(_background, Rect2(shield_bar_position - Vector2(2, 2), Vector2(92, 10))) + draw_rect(Rect2(shield_bar_position, Vector2(88 * _display_shield_ratio, 6)), Color.WHITE) + var hull_bar_position := bar_position + Vector2(0, 28) + draw_style_box(_background, Rect2(hull_bar_position - Vector2(2, 2), Vector2(92, 10))) + draw_rect(Rect2(hull_bar_position, Vector2(88 * _display_hull_ratio, 6)), Color("ef5967")) diff --git a/scripts/items/credit_pickup.gd b/scripts/items/credit_pickup.gd new file mode 100644 index 0000000..a39bc9e --- /dev/null +++ b/scripts/items/credit_pickup.gd @@ -0,0 +1,36 @@ +extends Node2D + +const PICKUP_RADIUS := 48.0 +const ATTRACTION_RADIUS := 150.0 +const POPUP := preload("res://scenes/game/damage_popup.tscn") +var amount: int = 0 +var _player: CharacterBody2D +var _collected := false + +func _ready() -> void: + add_to_group("credit_pickups") + _player = get_parent().get_node_or_null("PlayerShip") as CharacterBody2D + 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) + +func _physics_process(delta: float) -> void: + if _collected or not is_instance_valid(_player) or bool(_player.get("_is_destroyed")): + return + var distance := global_position.distance_to(_player.global_position) + if distance <= PICKUP_RADIUS: + _collected = true + GameState.captain.add_rewards(0, amount) + var popup := POPUP.instantiate() as Label + popup.text = "+%d KREDYTÓW" % amount + popup.set("text_color", Color("ffd56a")) + popup.position = _player.position + Vector2(-48, -60) + get_parent().add_child(popup) + set_physics_process(false) + queue_free() + elif distance <= ATTRACTION_RADIUS: + global_position = global_position.move_toward(_player.global_position, 180.0 * delta) diff --git a/scripts/items/credit_pickup.gd.uid b/scripts/items/credit_pickup.gd.uid new file mode 100644 index 0000000..122ad49 --- /dev/null +++ b/scripts/items/credit_pickup.gd.uid @@ -0,0 +1 @@ +uid://bgi4icgkhw4ax diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index d0dc3a6..7e7353d 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -129,4 +129,4 @@ func get_tooltip_text() -> String: return "\n".join(lines) static func stat_name(key: String) -> String: - return str({"damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) + return str({"shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 14eba06..35a2445 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -12,7 +12,7 @@ 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 := 6 -const STARTING_AMMUNITION := 300 +const STARTING_AMMUNITION := 50 const AMMUNITION_CRAFT_AMOUNT := 100 const AMMUNITION_CRAFT_PARTS_COST := 50.0 const AMMUNITION_BUY_AMOUNT := 100 @@ -215,7 +215,7 @@ func _recalculate_ship_stats() -> void: for item in weapon_slots + module_slots: if item == null or item.definition == null: continue - for stat_key in ["max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]: + for stat_key in ["max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent", "shield_on_damage_percent"]: bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key) ship.call("apply_equipment_bonuses", bonuses) diff --git a/tests/captain/captain_system_test.gd b/tests/captain/captain_system_test.gd index 0e00fd0..b7a828c 100644 --- a/tests/captain/captain_system_test.gd +++ b/tests/captain/captain_system_test.gd @@ -51,6 +51,8 @@ func _ready() -> void: enemy.call("take_weapon_hit", 1000000.0, true) enemy.call("take_damage", 1000000.0) assert(profile.level == 2 and profile.experience == 130 and profile.learning_points == 1) + assert(profile.credits == 0) + await _collect_credits(game.get_node("PlayerShip") as CharacterBody2D) assert(profile.credits == 180) var panel := game.get_node("HUD/HUDRoot/CaptainPanel") panel.get_node("Contents/Skills/gunner/Train_gunner").emit_signal("pressed") @@ -117,6 +119,7 @@ func _ready() -> void: assert(player.get_node("ShipInventory").call("get_item", "weapon", 0) == weapon) for enemy in get_tree().get_nodes_in_group("enemies"): enemy.call("take_damage", 1000000.0) + await _collect_credits(player) assert(profile.level == 4 and profile.experience == 68 and profile.credits == 452) var planet: Node2D = game.get("protected_planet") player.position = planet.position @@ -131,12 +134,20 @@ func _ready() -> void: game = get_tree().current_scene as Node2D _freeze(game) assert(GameState.captain == profile and GameState.system_level == 2) + player = game.get_node("PlayerShip") as CharacterBody2D assert(profile.get_skill_rank("gunner") == 1 and profile.get_skill_rank("sharp_eye") == 20) var enemy := get_tree().get_nodes_in_group("enemies")[0] as CharacterBody2D enemy.call("take_damage", 1000000.0) + await _collect_credits(player) assert(profile.level == 5 and profile.experience == 15 and profile.credits == 641) print("CAPTAIN_SYSTEM_TEST: PASS (XP, multiple levels, learning UI, rewards once, dodge, reduction, critical addons, projectile metadata, death and next system)") +func _collect_credits(player: CharacterBody2D) -> void: + await get_tree().process_frame + for pickup in get_tree().get_nodes_in_group("credit_pickups"): + player.global_position = pickup.global_position + pickup.call("_physics_process", 0.0) + func _test_skill_balance_and_reset() -> void: assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 1), 0.5 / 11.0)) assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 5), 1.0 / 6.0)) diff --git a/tests/items/credit_pickup_test.gd b/tests/items/credit_pickup_test.gd new file mode 100644 index 0000000..882fee4 --- /dev/null +++ b/tests/items/credit_pickup_test.gd @@ -0,0 +1,51 @@ +extends Node2D + +const PLAYER := preload("res://scenes/game/player_ship.tscn") +const ENEMY := preload("res://scenes/game/enemy_ship.tscn") +const BALANCE := preload("res://scripts/captain/captain_balance.gd") + +func _ready() -> void: + GameState.start_new_run() + GameState.system_level = 4 + 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) + ship.set("credits_multiplier", 1.5) + var inventory := ship.get_node("ShipInventory") as ShipInventory + assert(inventory.ammunition == 50) + var enemy := ENEMY.instantiate() as CharacterBody2D + enemy.position = Vector2(700, 0) + add_child(enemy) + enemy.set_physics_process(false) + enemy.set("_player", ship) + enemy.call("apply_level_settings", 4, 2) + var expected := roundi(BALANCE.KILL_CREDITS[2] * 1.15 * 1.5) + enemy.call("take_damage", enemy.get("max_hull")) + assert(GameState.captain.credits == 0) + assert(GameState.captain.level > 1 or GameState.captain.experience > 0) + await get_tree().process_frame + var pickups := get_tree().get_nodes_in_group("credit_pickups") + assert(pickups.size() == 1) + var pickup := pickups[0] as Node2D + pickup.set_physics_process(false) + assert(pickup.get("amount") == expected) + pickup.call("_physics_process", 0.1) + assert(GameState.captain.credits == 0) + # Credits can be collected even when cargo is full. + var item := ItemInstance.new() + item.definition = preload("res://resources/items/plasma_cannon.tres") + inventory.cargo.fill(item) + ship.position = pickup.position + pickup.call("_physics_process", 0.1) + assert(GameState.captain.credits == expected) + pickup.call("_physics_process", 0.1) + assert(GameState.captain.credits == expected) + var found_popup := false + for child in get_children(): + if child is Label and child.text == "+%d KREDYTÓW" % expected: + found_popup = true + assert(found_popup) + print("CREDIT_PICKUP_TEST: PASS (drop, deferred credits, XP, gear bonus, proximity, full cargo, popup, single collection)") + get_tree().quit() diff --git a/tests/items/credit_pickup_test.gd.uid b/tests/items/credit_pickup_test.gd.uid new file mode 100644 index 0000000..1731d84 --- /dev/null +++ b/tests/items/credit_pickup_test.gd.uid @@ -0,0 +1 @@ +uid://dcwgf8tg8tncy diff --git a/tests/items/credit_pickup_test.tscn b/tests/items/credit_pickup_test.tscn new file mode 100644 index 0000000..9b2b372 --- /dev/null +++ b/tests/items/credit_pickup_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/credit_pickup_test.gd" id="1"] +[node name="CreditPickupTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/machine_gun_shield_recovery_test.gd b/tests/items/machine_gun_shield_recovery_test.gd new file mode 100644 index 0000000..736c78c --- /dev/null +++ b/tests/items/machine_gun_shield_recovery_test.gd @@ -0,0 +1,99 @@ +extends Node2D + +const PLAYER := preload("res://scenes/game/player_ship.tscn") +const ENEMY := preload("res://scenes/game/enemy_ship.tscn") + +class AimedGun: + extends "res://scripts/game/weapon_controller.gd" + func _aim_direction_from(_origin: Vector2) -> Vector2: + return Vector2.RIGHT + +func _ready() -> void: + get_tree().create_timer(20.0).timeout.connect(func() -> void: + push_error("MACHINE_GUN_SHIELD_RECOVERY_TEST: timed out") + Input.action_release("ship_fire_primary") + get_tree().quit(1) + ) + seed(12345) + 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) + ship.set("shields", 0.0) + ship.set("energy", 0.0) + var inventory := ship.get_node("ShipInventory") as ShipInventory + var gun := ItemInstance.new() + gun.definition = preload("res://resources/items/machine_gun.tres") + assert(inventory.add_item(gun)) + assert(inventory.move_item("cargo", 0, "weapon", 0)) + var module := ItemInstance.new() + module.definition = preload("res://resources/items/engine_extension.tres") + var addon := ItemStatModifier.new() + addon.affix = preload("res://resources/items/affixes/shield_on_damage.tres") + addon.stat_key = addon.affix.stat_key + addon.value = addon.affix.roll_value(1) + module.additional_modifiers.append(addon) + assert(inventory.add_item(module)) + assert(inventory.move_item("cargo", inventory.cargo.find(module), "module", 0)) + assert(ship.get("shield_on_damage_percent") == 1.0) + var indicator := Node2D.new() + indicator.set_script(preload("res://scripts/game/ship_energy_indicator.gd")) + indicator.set("player", ship) + ship.add_child(indicator) + var enemy := ENEMY.instantiate() as CharacterBody2D + enemy.position = Vector2(240, -28) + add_child(enemy) + enemy.set_physics_process(false) + enemy.set("_player", ship) + enemy.set("max_hull", 10000.0) + enemy.set("hull", 10000.0) + var controller := AimedGun.new() + ship.add_child(controller) + await get_tree().physics_frame + await get_tree().physics_frame + # Fire a real projectile with the smallest level-1 damage: 2 DMG. + gun.damage_min = 2.0 + gun.damage_max = 2.0 + Input.action_press("ship_fire_primary") + for frame in 60: + await get_tree().physics_frame + if inventory.ammunition < 50: + break + Input.action_release("ship_fire_primary") + assert(inventory.ammunition == 49) + for frame in 60: + await get_tree().physics_frame + if enemy.get("hull") < 10000.0: + break + assert(is_equal_approx(enemy.get("hull"), 9998.0)) + assert(is_equal_approx(ship.get("shields"), 0.02)) + print("MACHINE_GUN_RECOVERY: single physical hit: 2 DMG -> +%.4f shields" % ship.get("shields")) + # Restore the normal starter roll (3–5 DMG), then empty the remaining magazine. + gun.damage_min = -1.0 + gun.damage_max = -1.0 + Input.action_press("ship_fire_primary") + for frame in 600: + await get_tree().physics_frame + if inventory.ammunition == 0: + break + Input.action_release("ship_fire_primary") + assert(inventory.ammunition == 0) + for frame in 45: + await get_tree().physics_frame + var actual_damage := 10000.0 - float(enemy.get("hull")) + var recovered := float(ship.get("shields")) + assert(is_equal_approx(recovered, actual_damage * 0.01)) + assert(recovered > 1.0 and ship.get("energy") == 0.0) + indicator.call("_process", 1.0) + assert(is_equal_approx(indicator.get("_display_shield_ratio"), recovered / float(ship.get("max_shields")))) + print("MACHINE_GUN_RECOVERY: 50 physical hits, %.0f total DMG -> +%.4f shields; white bar: %.4f%% (%.3f px)" % [actual_damage, recovered, recovered / float(ship.get("max_shields")) * 100.0, recovered / float(ship.get("max_shields")) * 88.0]) + # Empty ammo cannot keep firing or generate any further shields. + Input.action_press("ship_fire_primary") + for frame in 30: + await get_tree().physics_frame + Input.action_release("ship_fire_primary") + assert(is_equal_approx(ship.get("shields"), recovered)) + print("MACHINE_GUN_SHIELD_RECOVERY_TEST: PASS (real firing input, projectile flight, collisions, 2-DMG fractions, accumulated recovery, ammo consumption, zero energy, shield bar, empty ammo)") + get_tree().quit() diff --git a/tests/items/machine_gun_shield_recovery_test.tscn b/tests/items/machine_gun_shield_recovery_test.tscn new file mode 100644 index 0000000..8d02afd --- /dev/null +++ b/tests/items/machine_gun_shield_recovery_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/machine_gun_shield_recovery_test.gd" id="1"] +[node name="MachineGunShieldRecoveryTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/shield_on_damage_test.gd b/tests/items/shield_on_damage_test.gd new file mode 100644 index 0000000..8d1f307 --- /dev/null +++ b/tests/items/shield_on_damage_test.gd @@ -0,0 +1,76 @@ +extends Node2D + +const PLAYER := preload("res://scenes/game/player_ship.tscn") +const ENEMY := preload("res://scenes/game/enemy_ship.tscn") +const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") + +func _ready() -> void: + GameState.start_new_run() + var ship := PLAYER.instantiate() as CharacterBody2D + ship.name = "PlayerShip" + add_child(ship) + ship.set_physics_process(false) + var weapons := ship.get_node("WeaponController") + weapons.set_physics_process(false) + var inventory := ship.get_node("ShipInventory") as ShipInventory + var modifier := ItemStatModifier.new() + modifier.stat_key = "shield_on_damage_percent" + modifier.value = 1.0 + var module := ItemInstance.new() + module.definition = preload("res://resources/items/engine_extension.tres") + module.additional_modifiers.append(modifier) + assert(inventory.add_item(module)) + assert(inventory.move_item("cargo", 0, "module", 0)) + assert(ship.get("shield_on_damage_percent") == 1.0) + ship.set("shields", 0.0) + var enemy := _enemy(ship, 100.0) + # A gun without the addon still benefits from the global module stat. + weapons.call("_apply_damage", enemy, 20.0, false) + assert(is_equal_approx(ship.get("shields"), 0.2)) + var projectile := PROJECTILE.instantiate() as Area2D + projectile.set("damage", 10.0) + projectile.set("is_critical", true) + add_child(projectile) + projectile.set_physics_process(false) + projectile.call("_apply_damage_once", enemy) + projectile.call("_apply_damage_once", enemy) + assert(is_equal_approx(ship.get("shields"), 0.3)) + enemy.call("take_weapon_hit", 1000.0, true) + assert(is_equal_approx(ship.get("shields"), 1.0)) # Only remaining 70 HP. + enemy.call("take_weapon_hit", 1000.0, true) + assert(is_equal_approx(ship.get("shields"), 1.0)) + # Addons on weapons and modules sum globally. + inventory.weapon_slots[0].additional_modifiers.append(modifier) + inventory.call("_recalculate_ship_stats") + assert(ship.get("shield_on_damage_percent") == 2.0) + enemy = _enemy(ship, 100.0) + enemy.call("take_damage", 10.0) + assert(is_equal_approx(ship.get("shields"), 1.0)) + enemy.call("take_weapon_hit", -5.0, false) + assert(is_equal_approx(ship.get("shields"), 1.0)) + enemy.call("take_weapon_hit", 50.0, false) + assert(is_equal_approx(ship.get("shields"), 2.0)) + ship.set("shields", ship.get("max_shields") - 0.1) + enemy.call("take_weapon_hit", 10.0, false) + assert(ship.get("shields") == ship.get("max_shields")) + assert(inventory.move_item("module", 0, "cargo", 0)) + inventory.weapon_slots[0].additional_modifiers.clear() + inventory.call("_recalculate_ship_stats") + ship.set("shields", 0.0) + enemy.call("take_weapon_hit", 10.0, false) + assert(ship.get("shields") == 0.0) + var affix := load("res://resources/items/affixes/shield_on_damage.tres") as ItemAffixDefinition + assert(affix.roll_value(1) == 1.0 and affix.roll_value(1000) == 5.0) + assert(affix.can_roll(module.definition, 1)) + assert(affix.can_roll(inventory.weapon_slots[0].definition, 1)) + print("SHIELD_ON_DAMAGE_TEST: PASS (global addon, laser/projectile hits, stacking, actual HP, overkill, duplicate hits, caps, unequipping, scaling)") + get_tree().quit() + +func _enemy(ship: CharacterBody2D, hp: float) -> CharacterBody2D: + var enemy := ENEMY.instantiate() as CharacterBody2D + enemy.position = Vector2(700, 0) + add_child(enemy) + enemy.set_physics_process(false) + enemy.set("_player", ship) + enemy.set("hull", hp) + return enemy diff --git a/tests/items/shield_on_damage_test.gd.uid b/tests/items/shield_on_damage_test.gd.uid new file mode 100644 index 0000000..000725d --- /dev/null +++ b/tests/items/shield_on_damage_test.gd.uid @@ -0,0 +1 @@ +uid://fcx7ivtsrkcp diff --git a/tests/items/shield_on_damage_test.tscn b/tests/items/shield_on_damage_test.tscn new file mode 100644 index 0000000..2232565 --- /dev/null +++ b/tests/items/shield_on_damage_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/shield_on_damage_test.gd" id="1"] +[node name="ShieldOnDamageTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd index 69c115b..97280eb 100644 --- a/tests/items/weapon_damage_test.gd +++ b/tests/items/weapon_damage_test.gd @@ -77,7 +77,7 @@ func _ready() -> void: player.add_child(probe) probe.set_physics_process(false) var inventory := player.get_node("ShipInventory") as ShipInventory - assert(inventory.ammunition == 300) + assert(inventory.ammunition == ShipInventory.STARTING_AMMUNITION) var machine_gun := ItemInstance.new() machine_gun.definition = preload("res://resources/items/machine_gun.tres") player.set("energy", 0.0)