From 897123c7c82441ca2ef3252254b777afdb61d1b7 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sun, 11 Oct 2026 17:16:45 +0200 Subject: [PATCH] Zmiany w skilach --- scenes/ui/planet_services_menu.tscn | 7 +++ scripts/captain/captain_balance.gd | 24 +++++++-- scripts/captain/captain_progress.gd | 37 +++++++++++++ scripts/game/engine_visual.gd | 24 ++++++--- scripts/game/flight_hud.gd | 1 + scripts/game/ship_controller.gd | 63 ++++++++++++++++++---- scripts/game/ship_engine_audio.gd | 2 +- scripts/items/item_instance.gd | 6 ++- scripts/items/item_stat_modifier.gd | 3 +- scripts/ui/captain_panel.gd | 14 +++-- scripts/ui/planet_services_menu.gd | 9 ++++ tests/abilities/mini_boost_test.gd | 72 ++++++++++++++++++++++++++ tests/abilities/mini_boost_test.gd.uid | 1 + tests/abilities/mini_boost_test.tscn | 4 ++ tests/captain/captain_system_test.gd | 72 +++++++++++++++++++++----- tests/levels/system_lifecycle_test.gd | 9 ++++ 16 files changed, 301 insertions(+), 47 deletions(-) create mode 100644 tests/abilities/mini_boost_test.gd create mode 100644 tests/abilities/mini_boost_test.gd.uid create mode 100644 tests/abilities/mini_boost_test.tscn diff --git a/scenes/ui/planet_services_menu.tscn b/scenes/ui/planet_services_menu.tscn index 907fdf1..9325d97 100644 --- a/scenes/ui/planet_services_menu.tscn +++ b/scenes/ui/planet_services_menu.tscn @@ -71,6 +71,13 @@ theme_override_font_sizes/font_size = 12 focus_mode = 0 text = "KUP 100 AMUNICJI — 100 KREDYTÓW" +[node name="ResetSkillsButton" type="Button" parent="Center/Panel/Contents"] +custom_minimum_size = Vector2(0, 36) +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +focus_mode = 0 +text = "RESET SKILLI — 100 KREDYTÓW" + [node name="NextButton" type="Button" parent="Center/Panel/Contents"] custom_minimum_size = Vector2(0, 44) layout_mode = 2 diff --git a/scripts/captain/captain_balance.gd b/scripts/captain/captain_balance.gd index 59dfa6e..6592e50 100644 --- a/scripts/captain/captain_balance.gd +++ b/scripts/captain/captain_balance.gd @@ -3,7 +3,8 @@ extends RefCounted const XP_BASE := 100 const XP_PER_LEVEL := 50 const LEARNING_POINTS_PER_LEVEL := 1 -const MAX_SKILL_RANK := 100 +const MAX_SKILL_RANK := 20 +const SKILL_RESET_COST_PER_LEVEL := 100 const BASE_CRITICAL_MULTIPLIER := 2.0 const KILL_XP := [20, 50, 150] const KILL_CREDITS := [15, 40, 120] @@ -12,11 +13,24 @@ const SKILL_NAMES := { "mechanic": "Mechanik", "sharp_eye": "Celne oko", } const SKILL_HELP := { - "piloting": "Każdy poziom: +1 punkt procentowy szansy na unik. Unik anuluje całe trafienie.", - "gunner": "Każdy poziom: +1% obrażeń wszystkich broni.", - "mechanic": "Każdy poziom: otrzymywane obrażenia mniejsze o 1% (przed tarczami i kadłubem).", - "sharp_eye": "Każdy poziom: +1 punkt procentowy szansy na trafienie krytyczne.", + "piloting": "Malejące korzyści: 37,5% × punkty / (punkty + 10). Maksymalnie 25% uniku przy 20 punktach. Unik anuluje całe trafienie.", + "gunner": "Każdy punkt: +2% obrażeń wszystkich broni. Maksymalnie +40% przy 20 punktach.", + "mechanic": "Malejące korzyści: 50% × punkty / (punkty + 10). Maksymalnie 33,3% redukcji przy 20 punktach, przed tarczami i kadłubem.", + "sharp_eye": "Każdy punkt: +1 punkt procentowy szansy na trafienie krytyczne. Maksymalnie +20 przy 20 punktach.", } +static func skill_bonus(skill: String, rank: int) -> float: + var points := float(clampi(rank, 0, MAX_SKILL_RANK)) + match skill: + "piloting": + return 0.375 * points / (points + 10.0) + "mechanic": + return 0.5 * points / (points + 10.0) + "gunner": + return 0.02 * points + "sharp_eye": + return 0.01 * points + return 0.0 + static func xp_required(level: int) -> int: return XP_BASE + XP_PER_LEVEL * (maxi(1, level) - 1) diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 87e2dc8..da59f4d 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -18,6 +18,43 @@ func get_xp_required() -> int: func get_skill_rank(skill: String) -> int: return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK) +func get_skill_bonus(skill: String) -> float: + return BALANCE.skill_bonus(skill, get_skill_rank(skill)) + +func normalize_skill_ranks() -> void: + var changed_ranks := false + for skill in BALANCE.SKILL_NAMES: + var previous := maxi(0, int(skills.get(skill, 0))) + var rank := mini(previous, BALANCE.MAX_SKILL_RANK) + learning_points += previous - rank + if int(skills.get(skill, 0)) != rank: + changed_ranks = true + skills[skill] = rank + if changed_ranks: + emit_changed() + +func get_spent_learning_points() -> int: + var spent := 0 + for skill in BALANCE.SKILL_NAMES: + spent += maxi(0, int(skills.get(skill, 0))) + return spent + +func get_skill_reset_cost() -> int: + return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level) + +func can_reset_skills() -> bool: + return get_spent_learning_points() > 0 and credits >= get_skill_reset_cost() + +func reset_skills() -> bool: + if not can_reset_skills(): + return false + credits -= get_skill_reset_cost() + learning_points += get_spent_learning_points() + for skill in BALANCE.SKILL_NAMES: + skills[skill] = 0 + emit_changed() + return true + func can_train(skill: String) -> bool: return skills.has(skill) and learning_points > 0 and get_skill_rank(skill) < BALANCE.MAX_SKILL_RANK diff --git a/scripts/game/engine_visual.gd b/scripts/game/engine_visual.gd index 7db34c3..bc765fe 100644 --- a/scripts/game/engine_visual.gd +++ b/scripts/game/engine_visual.gd @@ -8,6 +8,7 @@ var _thrust_strength := 0.0 var _engine_phase := 0.0 var _trail_emission_timer := 0.0 var _trail_particles: Array[Dictionary] = [] +var _mini_boost := false func _ready() -> void: _ship = get_parent() as CharacterBody2D @@ -24,7 +25,8 @@ func _process(delta: float) -> void: global_position = _ship.global_position global_rotation = _ship.global_rotation _engine_phase += delta - var thrust_active := Input.is_action_pressed("ship_thrust") + _mini_boost = bool(_ship.call("is_mini_boosting")) + var thrust_active := Input.is_action_pressed("ship_thrust") or _mini_boost if bool(_ship.get("_is_destroyed")): thrust_active = false var target_strength := 1.0 if thrust_active else 0.0 @@ -52,6 +54,7 @@ func _spawn_trail_particle() -> void: "age": 0.0, "life": life, "size": randf_range(1.5, 3.0), + "boost": _mini_boost, }) func _update_trail_particles(delta: float) -> void: @@ -69,7 +72,10 @@ func _draw() -> void: return var pulse := 0.82 + 0.18 * sin(_engine_phase * 25.0) - var length := (28.0 + pulse * 14.0) * _thrust_strength + var length := (28.0 + pulse * 14.0) * _thrust_strength * (1.35 if _mini_boost else 1.0) + var outer_color := Color(1.0, 0.28, 0.03) if _mini_boost else Color(0.04, 0.58, 1.0) + var inner_color := Color(1.0, 0.62, 0.12) if _mini_boost else Color(0.24, 0.9, 1.0) + var core_color := Color(1.0, 0.92, 0.7) if _mini_boost else Color(0.88, 0.98, 1.0) var base_y := 22.0 var outer_tip := base_y + length var inner_tip := base_y + length * 0.76 @@ -78,15 +84,15 @@ func _draw() -> void: draw_colored_polygon(PackedVector2Array([ Vector2(-5.2, base_y), Vector2(-3.2, base_y + 6.0), Vector2(0.0, outer_tip), Vector2(3.2, base_y + 6.0), Vector2(5.2, base_y), - ]), Color(0.04, 0.58, 1.0, 0.48 * _thrust_strength)) + ]), Color(outer_color, 0.48 * _thrust_strength)) draw_colored_polygon(PackedVector2Array([ Vector2(-2.8, base_y), Vector2(-1.8, base_y + 4.0), Vector2(0.0, inner_tip), Vector2(1.8, base_y + 4.0), Vector2(2.8, base_y), - ]), Color(0.24, 0.9, 1.0, 0.78 * _thrust_strength)) + ]), Color(inner_color, 0.78 * _thrust_strength)) draw_colored_polygon(PackedVector2Array([ Vector2(-1.0, base_y), Vector2(0.0, core_tip), Vector2(1.0, base_y), - ]), Color(0.88, 0.98, 1.0, 0.92 * _thrust_strength)) - draw_circle(Vector2(0.0, base_y), 2.3, Color(0.42, 0.88, 1.0, 0.82 * _thrust_strength)) + ]), Color(core_color, 0.92 * _thrust_strength)) + draw_circle(Vector2(0.0, base_y), 2.3, Color(inner_color, 0.82 * _thrust_strength)) func _draw_trail() -> void: for particle in _trail_particles: @@ -98,5 +104,7 @@ func _draw_trail() -> void: var particle_size := float(particle["size"]) var current_local := to_local(current_position) var tail_local := to_local(current_position - particle_velocity.normalized() * particle_size * 3.5) - draw_line(tail_local, current_local, Color(0.22, 0.78, 1.0, fade * 0.52), particle_size * 0.72, true) - draw_circle(current_local, particle_size * fade, Color(0.62, 0.94, 1.0, fade * 0.72)) + var trail_color := Color(1.0, 0.42, 0.06) if bool(particle.get("boost", false)) else Color(0.22, 0.78, 1.0) + var particle_color := Color(1.0, 0.76, 0.3) if bool(particle.get("boost", false)) else Color(0.62, 0.94, 1.0) + draw_line(tail_local, current_local, Color(trail_color, fade * 0.52), particle_size * 0.72, true) + draw_circle(current_local, particle_size * fade, Color(particle_color, fade * 0.72)) diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index c8019ae..7605c01 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -72,6 +72,7 @@ func _process(_delta: float) -> void: var to_planet := goal.global_position - player.global_position debug_info.text = """STEROWANIE W: ciąg S: hamulec / wsteczny +Tab: mini boost (+100%% prędkości, 20 energii/s) Q / E: strafe A / D: obrót LPM / PPM: broń 1 / broń 2 1–6: aktywne rozszerzenia diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 8892ffe..0bf308c 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -7,6 +7,7 @@ const COMBAT_ROLLS := preload("res://scripts/game/combat_rolls.gd") const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd") const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd") const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd") +const MINI_BOOST_ENERGY_PER_SECOND := 20.0 @export var max_speed := 76.0 @export var reverse_max_speed := 210.0 @@ -51,6 +52,9 @@ var _boost_speed := 0.0 var _boost_target := 0.0 var _boost_acceleration := 0.0 var _boost_velocity_acceleration := 0.0 +var _mini_boost_active := false +var _mini_boost_exhausted := false +var _mini_boost_coast_speed := 0.0 @onready var _shield_visual: Node2D = $ShieldVisual func _enter_tree() -> void: @@ -84,6 +88,7 @@ func _ready() -> void: for index in 6: _ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key) _ensure_input_action(&"ship_thrust", KEY_W) + _ensure_input_action(&"ship_mini_boost", KEY_TAB) _ensure_input_action(&"ship_brake", KEY_S) _ensure_input_action(&"ship_turn_left", KEY_A) _ensure_input_action(&"ship_turn_right", KEY_D) @@ -144,16 +149,16 @@ func take_damage(amount: float) -> void: _destroy_ship() func get_evasion_chance() -> float: - return GameState.captain.get_skill_rank("piloting") / 100.0 + return GameState.captain.get_skill_bonus("piloting") func get_damage_reduction() -> float: - return GameState.captain.get_skill_rank("mechanic") / 100.0 + return GameState.captain.get_skill_bonus("mechanic") func get_weapon_damage_multiplier() -> float: - return damage_multiplier * (1.0 + GameState.captain.get_skill_rank("gunner") / 100.0) + return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner")) func get_critical_chance() -> float: - return clampf(critical_chance_bonus + GameState.captain.get_skill_rank("sharp_eye") / 100.0, 0.0, 1.0) + return clampf(critical_chance_bonus + GameState.captain.get_skill_bonus("sharp_eye"), 0.0, 1.0) func get_critical_multiplier() -> float: return CAPTAIN_BALANCE.BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, critical_damage_bonus)) @@ -201,10 +206,19 @@ func _restart_level_after_destruction() -> void: func _physics_process(delta: float) -> void: if _is_destroyed or is_docked: + _mini_boost_active = false return _contact_damage_timer = maxf(0.0, _contact_damage_timer - delta) energy = minf(max_energy, energy + (energy_regeneration + get_solar_regeneration()) * delta) - var was_boosting := is_boosting() + var was_mini_boosting := _mini_boost_active + _update_mini_boost(delta) + if _mini_boost_active: + _mini_boost_coast_speed = 0.0 + else: + if was_mini_boosting: + _mini_boost_coast_speed = maxf(0.0, velocity.length() - max_speed) + _mini_boost_coast_speed = move_toward(_mini_boost_coast_speed, 0.0, space_drag * delta) + var was_boosting := _boost_target > 0.0 or _boost_speed > 0.0 if _boost_target > 0.0: _boost_speed = move_toward(_boost_speed, _boost_target, _boost_acceleration * delta) velocity = velocity.move_toward(Vector2.UP.rotated(rotation) * (max_speed + _boost_target), _boost_velocity_acceleration * delta) @@ -240,11 +254,12 @@ func _physics_process(delta: float) -> void: var reverse_speed := velocity.dot(forward) if reverse_speed < -reverse_max_speed: velocity += forward * (-reverse_max_speed - reverse_speed) - elif Input.is_action_pressed("ship_thrust"): - velocity += forward * thrust_acceleration * delta + elif Input.is_action_pressed("ship_thrust") or _mini_boost_active: + velocity += forward * thrust_acceleration * (2.0 if _mini_boost_active else 1.0) * delta var forward_speed := velocity.dot(forward) - if forward_speed > max_speed and not was_boosting: - velocity += forward * (max_speed - forward_speed) + var forward_limit := _get_allowed_speed() + if forward_speed > forward_limit and not was_boosting: + velocity += forward * (forward_limit - forward_speed) var strafe_input := Input.get_axis("ship_strafe_left", "ship_strafe_right") var right := Vector2.RIGHT.rotated(rotation) @@ -255,6 +270,7 @@ func _physics_process(delta: float) -> void: if ( not was_boosting + and not _mini_boost_active and not Input.is_action_pressed("ship_brake") and not Input.is_action_pressed("ship_thrust") @@ -269,13 +285,38 @@ func _physics_process(delta: float) -> void: func _clamp_velocity_to_max_speed() -> void: var speed_squared := velocity.length_squared() - var allowed_speed := max_speed + _boost_speed + var allowed_speed := _get_allowed_speed() var max_speed_squared := allowed_speed * allowed_speed if speed_squared > max_speed_squared: velocity = velocity.normalized() * allowed_speed func is_boosting() -> bool: - return _boost_target > 0.0 or _boost_speed > 0.0 + return _boost_target > 0.0 or _boost_speed > 0.0 or _mini_boost_active + +func is_mini_boosting() -> bool: + return _mini_boost_active and not _is_destroyed and not is_docked + +func _get_allowed_speed() -> float: + return maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed) + +func _update_mini_boost(delta: float) -> void: + _mini_boost_active = false + if not Input.is_action_pressed("ship_mini_boost"): + _mini_boost_exhausted = false + return + if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"): + return + var cost := MINI_BOOST_ENERGY_PER_SECOND * delta + if energy < cost or energy <= 0.0: + _mini_boost_exhausted = true + return + energy = maxf(0.0, energy - cost) + _mini_boost_active = true + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.is_action("ship_mini_boost"): + # Tab controls the engine rather than moving focus between HUD buttons. + get_viewport().set_input_as_handled() func get_solar_regeneration() -> float: if _is_destroyed or is_docked or not is_instance_valid(charging_star): diff --git a/scripts/game/ship_engine_audio.gd b/scripts/game/ship_engine_audio.gd index 9ab7795..0c293e1 100644 --- a/scripts/game/ship_engine_audio.gd +++ b/scripts/game/ship_engine_audio.gd @@ -24,7 +24,7 @@ func _process(delta: float) -> void: if stream == null or not is_instance_valid(_ship): return - var thrust_active := Input.is_action_pressed("ship_thrust") + var thrust_active := Input.is_action_pressed("ship_thrust") or bool(_ship.call("is_mini_boosting")) if bool(_ship.get("_is_destroyed")): thrust_active = false var target_volume := THRUST_VOLUME_DB if thrust_active else SILENT_VOLUME_DB diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index 77e2799..3f7905f 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -1,6 +1,8 @@ extends Resource class_name ItemInstance +const ENERGY_REGENERATION_BONUS_MULTIPLIER := 0.5 + @export var definition: ItemDefinition @export_range(1, 1000, 1) var item_level: int = 1 @export var additional_modifiers: Array[ItemStatModifier] = [] @@ -22,7 +24,7 @@ func get_stat_total(stat_key: String) -> float: for modifier in additional_modifiers: if modifier and modifier.stat_key == stat_key: total += modifier.value - return total + return total * ENERGY_REGENERATION_BONUS_MULTIPLIER if stat_key == "energy_regeneration" else total func get_damage_range() -> Vector2: if definition == null or definition.category != ItemDefinition.Category.WEAPON: @@ -108,7 +110,7 @@ func get_tooltip_text() -> String: primary_text = definition.active_ability.description var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text] if definition.energy_regeneration_bonus > 0.0: - lines.append("Regeneracja generatora: +%.1f /s" % (energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus)) + lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER)) if definition.category == ItemDefinition.Category.WEAPON: lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: diff --git a/scripts/items/item_stat_modifier.gd b/scripts/items/item_stat_modifier.gd index 0eb318b..271c81b 100644 --- a/scripts/items/item_stat_modifier.gd +++ b/scripts/items/item_stat_modifier.gd @@ -8,4 +8,5 @@ class_name ItemStatModifier func get_display_text() -> String: var label := affix.display_name if affix else ItemInstance.stat_name(stat_key) var suffix := affix.unit if affix else ("%" if stat_key.ends_with("_percent") else "/s") - return "%s: %s%.2f %s" % [label, "+" if value >= 0.0 else "", value, suffix] + var displayed_value := value * ItemInstance.ENERGY_REGENERATION_BONUS_MULTIPLIER if stat_key == "energy_regeneration" else value + return "%s: %s%.2f %s" % [label, "+" if displayed_value >= 0.0 else "", displayed_value, suffix] diff --git a/scripts/ui/captain_panel.gd b/scripts/ui/captain_panel.gd index 8447dd0..79c9118 100644 --- a/scripts/ui/captain_panel.gd +++ b/scripts/ui/captain_panel.gd @@ -17,6 +17,7 @@ var _buttons: Dictionary = {} func _ready() -> void: _captain = GameState.captain + _captain.normalize_skill_ranks() for skill in BALANCE.SKILL_NAMES: var row := HBoxContainer.new() row.name = skill @@ -28,13 +29,13 @@ func _ready() -> void: title.add_theme_font_size_override("font_size", 11) row.add_child(title) var rank := Label.new() - rank.custom_minimum_size.x = 22 + rank.custom_minimum_size.x = 40 rank.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT rank.add_theme_font_size_override("font_size", 11) row.add_child(rank) _rank_labels[skill] = rank var effect := Label.new() - effect.custom_minimum_size.x = 58 + effect.custom_minimum_size.x = 74 effect.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT effect.add_theme_font_size_override("font_size", 10) row.add_child(effect) @@ -65,8 +66,11 @@ func _refresh() -> void: points_label.text = "Punkty: %d" % _captain.learning_points for skill in _buttons: var rank := _captain.get_skill_rank(skill) - _rank_labels[skill].text = str(rank) - var suffix: String = {"piloting": "unik", "gunner": "DMG", "mechanic": "redukcji", "sharp_eye": "kryt"}[skill] - _effect_labels[skill].text = "%d%% %s" % [rank, suffix] + _rank_labels[skill].text = "%d/%d" % [rank, BALANCE.MAX_SKILL_RANK] + var suffix: String = {"piloting": "unik", "gunner": "DMG", "mechanic": "red.", "sharp_eye": "kryt"}[skill] + var bonus := _captain.get_skill_bonus(skill) + _effect_labels[skill].text = "%.1f%% %s" % [bonus * 100.0, suffix] + var next_bonus := BALANCE.skill_bonus(skill, rank + 1) + _buttons[skill].tooltip_text = BALANCE.SKILL_HELP[skill] + ("\nOsiągnięto limit 20 punktów." if rank >= BALANCE.MAX_SKILL_RANK else "\nNastępny punkt: %.1f%% → %.1f%%" % [bonus * 100.0, next_bonus * 100.0]) _buttons[skill].disabled = not _captain.can_train(skill) critical_label.text = "Krytyk: %.1f%% · ×%.2f\nCzęści: %.1f" % [float(player.call("get_critical_chance")) * 100.0, float(player.call("get_critical_multiplier")), _captain.parts] diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 1454b41..67aaf67 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -4,6 +4,7 @@ signal next_level_requested @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 @onready var services_label: Label = $Center/Panel/Contents/Services @onready var status_label: Label = $Center/Panel/Contents/Status @onready var panel: PanelContainer = $Center/Panel @@ -15,6 +16,7 @@ var _open_tween: Tween func _ready() -> void: next_button.pressed.connect(func() -> void: next_level_requested.emit()) buy_ammunition_button.pressed.connect(_buy_ammunition) + reset_skills_button.pressed.connect(_reset_skills) _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.ammunition_changed.connect(_refresh_ammunition_service) @@ -59,6 +61,9 @@ func _position_at_planet() -> void: 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 - 80.0))) func _refresh_ammunition_service() -> void: + 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() 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()] @@ -67,3 +72,7 @@ func _refresh_ammunition_service() -> void: 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/mini_boost_test.gd b/tests/abilities/mini_boost_test.gd new file mode 100644 index 0000000..25a3ade --- /dev/null +++ b/tests/abilities/mini_boost_test.gd @@ -0,0 +1,72 @@ +extends Node2D + +const PLAYER := preload("res://scenes/game/player_ship.tscn") + +func _ready() -> void: + GameState.start_new_run() + var ship := PLAYER.instantiate() as CharacterBody2D + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.set("energy_regeneration", 0.0) + ship.set("energy", 100.0) + var tab := InputEventKey.new() + tab.physical_keycode = KEY_TAB + assert(InputMap.action_has_event("ship_mini_boost", tab)) + Input.action_press("ship_mini_boost") + ship.call("_physics_process", 1.0) + assert(ship.call("is_mini_boosting")) + assert(is_equal_approx(ship.get("energy"), 80.0)) + assert(is_equal_approx(ship.velocity.length(), ship.get("max_speed") * 2.0)) + var engine := ship.get_node("EngineVisual") + engine.call("_process", 0.2) + assert(engine.get("_mini_boost")) + var particles: Array = engine.get("_trail_particles") + assert(not particles.is_empty() and particles[0]["boost"]) + Input.action_release("ship_mini_boost") + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting")) + var released_speed := ship.velocity.length() + assert(released_speed > ship.get("max_speed")) + assert(is_equal_approx(released_speed, ship.get("max_speed") * 2.0 - ship.get("space_drag") * 0.1)) + Input.action_press("ship_thrust") + ship.call("_physics_process", 0.1) + assert(is_equal_approx(ship.velocity.length(), released_speed - ship.get("space_drag") * 0.1)) + Input.action_release("ship_thrust") + for frame in 120: + ship.call("_physics_process", 0.1) + assert(ship.velocity.length() <= ship.get("max_speed")) + ship.set("energy", 0.0) + Input.action_press("ship_mini_boost") + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting")) + ship.set("energy", 100.0) + Input.action_release("ship_mini_boost") + ship.call("_physics_process", 0.1) + Input.action_press("ship_mini_boost") + ship.call("_physics_process", 1.0) + ship.set("energy", 0.0) + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting")) + assert(is_equal_approx(ship.velocity.length(), ship.get("max_speed") * 2.0 - ship.get("space_drag") * 0.1)) + ship.set("energy", 20.0) + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting") and ship.get("energy") == 20.0) + Input.action_release("ship_mini_boost") + ship.call("_physics_process", 0.1) + Input.action_press("ship_mini_boost") + ship.call("_physics_process", 0.1) + assert(ship.call("is_mini_boosting") and ship.get("energy") == 18.0) + Input.action_press("ship_brake") + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting") and ship.get("energy") == 18.0) + Input.action_release("ship_brake") + ship.call("start_boost", 2.0, 0.5) + ship.set("_boost_speed", ship.get("max_speed") * 2.0) + ship.call("_physics_process", 0.1) + assert(ship.call("_get_allowed_speed") == ship.get("max_speed") * 3.0) + ship.set("is_docked", true) + ship.call("_physics_process", 0.1) + assert(not ship.call("is_mini_boosting")) + Input.action_release("ship_mini_boost") + print("MINI_BOOST_TEST: PASS (Tab mapping, double speed, energy drain, orange trail, release, exhaustion latch, braking, afterburner and docking)") diff --git a/tests/abilities/mini_boost_test.gd.uid b/tests/abilities/mini_boost_test.gd.uid new file mode 100644 index 0000000..ec02a68 --- /dev/null +++ b/tests/abilities/mini_boost_test.gd.uid @@ -0,0 +1 @@ +uid://borjkb545msdf diff --git a/tests/abilities/mini_boost_test.tscn b/tests/abilities/mini_boost_test.tscn new file mode 100644 index 0000000..ea0905c --- /dev/null +++ b/tests/abilities/mini_boost_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/abilities/mini_boost_test.gd" id="1"] +[node name="MiniBoostTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/captain/captain_system_test.gd b/tests/captain/captain_system_test.gd index f73b1e5..0e00fd0 100644 --- a/tests/captain/captain_system_test.gd +++ b/tests/captain/captain_system_test.gd @@ -5,6 +5,7 @@ const COMBAT := preload("res://scripts/game/combat_rolls.gd") const GAME_SCENE := preload("res://scenes/game/game.tscn") const GENERATOR := preload("res://scripts/items/item_generator.gd") const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") +const BALANCE := preload("res://scripts/captain/captain_balance.gd") class DamageReceiver: extends Node2D @@ -13,6 +14,7 @@ class DamageReceiver: hits.append({"damage": amount, "critical": critical}) func _ready() -> void: + _test_skill_balance_and_reset() var profile := PROGRESS.new() assert(not profile.train_skill("gunner")) profile.add_rewards(99, 60) @@ -58,20 +60,21 @@ func _ready() -> void: assert(panel.get_node("Contents/Header/Level").get("text") == "POZ. 2") var player := game.get_node("PlayerShip") as CharacterBody2D profile.skills["piloting"] = 100 - var before := Vector2(float(player.get("hull")), float(player.get("shields"))) - player.call("take_damage", 1000.0) - assert(before == Vector2(float(player.get("hull")), float(player.get("shields")))) + assert(is_equal_approx(float(player.call("get_evasion_chance")), 0.25)) + assert(COMBAT.incoming_hit(100, player.call("get_evasion_chance"), 0, 0.0)["evaded"]) + assert(COMBAT.incoming_hit(100, player.call("get_evasion_chance"), 0, 0.25)["damage"] == 100) profile.skills["piloting"] = 0 - profile.skills["mechanic"] = 50 + profile.skills["mechanic"] = 15 player.set("shields", 100.0) # Fixed fixture, independent of selected chassis capacity. player.call("take_damage", 100.0) - assert(float(player.get("shields")) == 50 and float(player.get("hull")) == 100) + assert(is_equal_approx(float(player.get("shields")), 30) and float(player.get("hull")) == 100) player.set("shields", 0.0) player.call("take_damage", 40.0) - assert(float(player.get("hull")) == 80) + assert(is_equal_approx(float(player.get("hull")), 72)) profile.skills["mechanic"] = 100 - player.call("take_damage", 1000000.0) - assert(float(player.get("hull")) == 80) + assert(is_equal_approx(float(player.call("get_damage_reduction")), 1.0 / 3.0)) + player.call("take_damage", 30.0) + assert(is_equal_approx(float(player.get("hull")), 52)) profile.skills["mechanic"] = 0 profile.skills["sharp_eye"] = 1 var inventory := player.get_node("ShipInventory") as ShipInventory @@ -88,20 +91,23 @@ func _ready() -> void: assert(inventory.add_item(weapon) and inventory.move_item("cargo", free_slot, "weapon", 0)) assert(is_equal_approx(float(player.call("get_critical_chance")), 0.26)) assert(is_equal_approx(float(player.call("get_critical_multiplier")), 2.4)) - assert(is_equal_approx(float(player.call("get_weapon_damage_multiplier")), 1.111)) - profile.skills["sharp_eye"] = 100 + assert(is_equal_approx(float(player.call("get_weapon_damage_multiplier")), 1.122)) + profile.skills["sharp_eye"] = 20 + assert(is_equal_approx(float(player.call("get_critical_chance")), 0.45)) + player.set("critical_chance_bonus", 0.8) # Force this metadata check independently of RNG. hit = player.call("roll_weapon_hit", weapon) - assert(hit["critical"] and is_equal_approx(hit["damage"], 26.664)) + assert(hit["critical"] and is_equal_approx(hit["damage"], 26.928)) + player.set("critical_chance_bonus", 0.25) var receiver := DamageReceiver.new() game.add_child(receiver) var projectile := PROJECTILE.instantiate() - projectile.call("configure", Vector2.ZERO, Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 400.0, 26.664, 1000.0, null, true) + projectile.call("configure", Vector2.ZERO, Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 400.0, 26.928, 1000.0, null, true) game.add_child(projectile) projectile.set_physics_process(false) projectile.call("_apply_damage_once", receiver) projectile.call("_apply_damage_once", receiver) assert(receiver.hits.size() == 1 and receiver.hits[0]["critical"]) - assert(is_equal_approx(receiver.hits[0]["damage"], 26.664)) + assert(is_equal_approx(receiver.hits[0]["damage"], 26.928)) player.call("take_damage", 1000000.0) await get_tree().create_timer(1.3).timeout game = get_tree().current_scene as Node2D @@ -125,12 +131,50 @@ func _ready() -> void: game = get_tree().current_scene as Node2D _freeze(game) assert(GameState.captain == profile and GameState.system_level == 2) - assert(profile.get_skill_rank("gunner") == 1 and profile.get_skill_rank("sharp_eye") == 100) + 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) 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 _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)) + assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 10), 0.25)) + assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 20), 1.0 / 3.0)) + assert(is_equal_approx(BALANCE.skill_bonus("piloting", 20), 0.25)) + assert(is_equal_approx(BALANCE.skill_bonus("gunner", 20), 0.4)) + assert(is_equal_approx(BALANCE.skill_bonus("sharp_eye", 20), 0.2)) + for skill in ["mechanic", "piloting"]: + var previous_gain := 1.0 + for rank in range(1, 21): + var gain := BALANCE.skill_bonus(skill, rank) - BALANCE.skill_bonus(skill, rank - 1) + assert(gain > 0.0 and gain < previous_gain) + previous_gain = gain + var captain := PROGRESS.new() + captain.learning_points = 100 + for rank in 20: + assert(captain.train_skill("mechanic")) + assert(not captain.train_skill("mechanic") and captain.learning_points == 80) + captain.level = 3 + captain.credits = 299 + assert(not captain.reset_skills()) + assert(captain.skills["mechanic"] == 20 and captain.credits == 299) + captain.credits = 300 + assert(captain.reset_skills()) + assert(captain.learning_points == 100 and captain.credits == 0 and captain.skills["mechanic"] == 0) + captain.credits = 300 + assert(not captain.reset_skills() and captain.credits == 300) + for skill in BALANCE.SKILL_NAMES: + captain.skills[skill] = 100 + captain.learning_points = 7 + captain.normalize_skill_ranks() + assert(captain.learning_points == 327 and captain.get_spent_learning_points() == 80) + captain.normalize_skill_ranks() + assert(captain.learning_points == 327) + assert(captain.reset_skills()) + assert(captain.learning_points == 407) + func _freeze(game: Node2D) -> void: game.set_physics_process(false) game.get_node("PlayerShip").set_physics_process(false) diff --git a/tests/levels/system_lifecycle_test.gd b/tests/levels/system_lifecycle_test.gd index a196f9f..a2c19a3 100644 --- a/tests/levels/system_lifecycle_test.gd +++ b/tests/levels/system_lifecycle_test.gd @@ -60,6 +60,15 @@ func _ready() -> void: GameState.captain.credits = inventory.get_ammunition_buy_cost() menu.get_node("Center/Panel/Contents/BuyAmmunitionButton").emit_signal("pressed") assert(inventory.ammunition == 400 and GameState.captain.credits == 0) + assert(GameState.captain.train_skill("gunner")) + GameState.captain.credits = GameState.captain.get_skill_reset_cost() + GameState.captain.emit_changed() + var reset_button: Button = menu.get_node("Center/Panel/Contents/ResetSkillsButton") + assert(not reset_button.disabled) + reset_button.emit_signal("pressed") + assert(GameState.captain.get_skill_rank("gunner") == 0) + assert(GameState.captain.learning_points == 1 and GameState.captain.credits == 0) + assert(reset_button.disabled) player.position = initial_data["player_start"] game.call("_physics_process", 0.0) assert(not menu.visible)