From a74319d01ef74c082e198ca410843ab30f0bd3ac Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Sat, 17 Oct 2026 19:46:45 +0200 Subject: [PATCH] balans --- scripts/captain/captain_progress.gd | 2 +- scripts/captain/captain_skills.gd | 5 ++- scripts/game/enemy_ship.gd | 10 +++-- scripts/levels/system_generator.gd | 56 +++++++++++++----------- scripts/ui/captain_skills_ui.gd | 2 +- tests/captain/captain_skills_test.gd | 6 ++- tests/captain/reactor_duration_test.gd | 23 ++++++++++ tests/captain/reactor_duration_test.tscn | 4 ++ tests/items/enemy_laser_test.gd | 8 ++-- tests/levels/wreck_rewards_test.gd.uid | 1 + 10 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 tests/captain/reactor_duration_test.gd create mode 100644 tests/captain/reactor_duration_test.tscn create mode 100644 tests/levels/wreck_rewards_test.gd.uid diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index cee7317..8a21dd9 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -174,7 +174,7 @@ func activate_skill(skill: String, ship: CharacterBody2D) -> bool: if not skill_unavailable_reason(skill, ship).is_empty(): return false skill_cooldowns[skill] = SKILLS.COOLDOWN - skill_durations[skill] = SKILLS.DURATION + skill_durations[skill] = SKILLS.duration(skill) if skill == "power_redirect": ship.set("shields", 0.0) ship.call("_update_shield_visual") diff --git a/scripts/captain/captain_skills.gd b/scripts/captain/captain_skills.gd index 0bfb0f0..1b67737 100644 --- a/scripts/captain/captain_skills.gd +++ b/scripts/captain/captain_skills.gd @@ -21,6 +21,9 @@ static func value(skill: String, rank: int) -> float: return 0.0 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 + static func icon(skill: String) -> Texture2D: return load("res://assets/icons/captain/%s.png" % skill) as Texture2D @@ -36,7 +39,7 @@ static func description(skill: String, rank: int) -> String: "business_shark": return "Zakupy za kredyty tańsze o %d%%." % amount "reactor_overload": - return "Regeneracja energii +%d%% przez 5 sekund." % amount + return "Regeneracja energii +%d%% przez 15 sekund." % amount "tactical_maneuver": return "Szansa uniku wynosi %d%% przez 5 sekund. Unik anuluje całe trafienie." % amount "efficient_magazine": diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 6f84c0e..b75cb3b 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -9,7 +9,8 @@ const LOOT_OFFSET := Vector2(0, 36) const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd") const LASER_COOLDOWN_MULTIPLIER := 2.0 const LASER_MINIMUM_COOLDOWN := 1.4 -const LASER_MISS_CHANCE := 0.35 +const LASER_MISS_CHANCE := 0.45 +const LASER_DAMAGE_MULTIPLIER := 0.8 @export_enum("Normal", "Elite", "Boss") var loot_tier: int = 0 @@ -275,7 +276,7 @@ func _fire_plasma(direction: Vector2) -> void: endpoint = hit["position"] var target: Node = hit.get("collider") if target is CharacterBody2D and target.has_method("take_damage"): - target.take_damage(weapon_damage, false, DamageTypes.Type.ENERGY) + target.take_damage(get_shot_damage(), false, DamageTypes.Type.ENERGY) var beam := Line2D.new() beam.set_script(preload("res://scripts/game/laser_beam_visual.gd")) beam.width = 4.0 @@ -335,9 +336,12 @@ func _laser_aim_direction(direction: Vector2) -> Vector2: return (direction * maxf(1.0, distance) + side * offset).normalized() return direction.rotated(randf_range(-0.02, 0.02)) +func get_shot_damage() -> float: + return weapon_damage * (LASER_DAMAGE_MULTIPLIER if weapon_kind == ItemDefinition.WeaponKind.LASER else 1.0) + func get_weapon_dps() -> float: var interval := get_fire_interval() - return weapon_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(3.0, interval) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0) func _reward_captain() -> void: if is_instance_valid(_player): diff --git a/scripts/levels/system_generator.gd b/scripts/levels/system_generator.gd index 139bd0d..053ce5a 100644 --- a/scripts/levels/system_generator.gd +++ b/scripts/levels/system_generator.gd @@ -6,7 +6,7 @@ const ALIEN_SHIPS := preload("res://scripts/items/alien_ship_catalog.gd") const WORLD_SIZE := Vector2(5200, 3600) const PLAYER_START := Vector2(180, 200) const SAFE_ZONE_RADIUS := 1200.0 -const ENEMY_MIN_SEPARATION := 110.0 +const ENEMY_MIN_SEPARATION := 420.0 const CELESTIAL_TEXTURES := preload("res://scripts/levels/celestial_texture_catalog.gd") const STAR_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.STAR_TEXTURES const PLANET_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.PLANET_TEXTURES @@ -46,23 +46,14 @@ static func generate(level: int, seed_value: int) -> Dictionary: ship_rng.seed = seed_value ^ (level * 0x53484950) var enemy_texture_index := ship_rng.randi_range(0, ALIEN_SHIPS.TEXTURES.size() - 1) var tiers := BALANCE.enemy_tiers(level) - var cursor := 0 - while cursor < tiers.size(): - var cluster_size := 3 if tiers[cursor] == 2 else rng.randi_range(2, 5) - cluster_size = mini(cluster_size, tiers.size() - cursor) - var planet: Dictionary = planets[rng.randi_range(0, planets.size() - 1)] - var anchor: Vector2 = planet["position"] - anchor += anchor.normalized() * rng.randf_range(250.0, 450.0) - for member in cluster_size: - var position := _find_enemy_position(anchor, planets, enemies, rng) - # Preserve the geometry stream formerly used to choose each ship. - rng.randi_range(0, 10) - enemies.append({ - "position": position, - "tier": tiers[cursor], - "texture_index": enemy_texture_index, - }) - cursor += 1 + # Spread ships across the playable area instead of packing them around planets. + for tier in tiers: + var position := _find_enemy_position(planets, enemies, rng) + enemies.append({ + "position": position, + "tier": tier, + "texture_index": enemy_texture_index, + }) return { "enemy_texture_index": enemy_texture_index, "star_name": star_name, @@ -74,16 +65,29 @@ static func generate(level: int, seed_value: int) -> Dictionary: "enemies": enemies, } -static func _find_enemy_position(anchor: Vector2, planets: Array[Dictionary], enemies: Array[Dictionary], rng: RandomNumberGenerator) -> Vector2: +static func _find_enemy_position(planets: Array[Dictionary], enemies: Array[Dictionary], rng: RandomNumberGenerator) -> Vector2: + var best := Vector2.ZERO + var best_distance := -1.0 + var valid_samples := 0 + # Prefer the clearest of several random positions to avoid accidental clusters. for attempt in 3000: - var candidate: Vector2 - if attempt < 500: - candidate = anchor + Vector2.RIGHT.rotated(rng.randf_range(0, TAU)) * rng.randf_range(130, 420) - else: - candidate = Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580)) - if _is_valid_enemy_position(candidate, planets, enemies): + var candidate := Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580)) + if not _is_valid_enemy_position(candidate, planets, enemies): + continue + if enemies.is_empty(): return candidate - # Deterministic fallback for crowded clusters, still respecting the safe area. + var nearest := INF + for enemy in enemies: + nearest = minf(nearest, candidate.distance_squared_to(enemy["position"])) + if nearest > best_distance: + best_distance = nearest + best = candidate + valid_samples += 1 + if valid_samples >= 16: + return best + if best_distance >= 0.0: + return best + # Deterministic fallback still respects spacing, planets and the starting area. for x in range(-2280, 2281, 130): for y in range(-1500, 1501, 130): var candidate := Vector2(x, y) diff --git a/scripts/ui/captain_skills_ui.gd b/scripts/ui/captain_skills_ui.gd index e00cad8..737f866 100644 --- a/scripts/ui/captain_skills_ui.gd +++ b/scripts/ui/captain_skills_ui.gd @@ -205,7 +205,7 @@ func _choice_card(skill: String) -> PanelContainer: description.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_child(description) if data["active"]: - body.add_child(_label("DZIAŁANIE 5 s · ODNOWIENIE 30 s", 11, Color("91aabd"))) + body.add_child(_label("DZIAŁANIE %d s · ODNOWIENIE 30 s" % int(CATALOG.duration(skill)), 11, Color("91aabd"))) else: body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd"))) var select := Button.new() diff --git a/tests/captain/captain_skills_test.gd b/tests/captain/captain_skills_test.gd index 27e1da8..4e1dc0d 100644 --- a/tests/captain/captain_skills_test.gd +++ b/tests/captain/captain_skills_test.gd @@ -75,13 +75,17 @@ func _ready() -> void: captain.tick_skills(4.9) assert(not captain.is_skill_active("power_redirect")) assert(ship.get_evasion_chance() == 0.0) - assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0)) + assert(captain.is_skill_active("reactor_overload")) + assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + CATALOG.value("reactor_overload", rank))) assert(is_equal_approx(captain.skill_cooldowns["reactor_overload"], 24.9)) ship._physics_process(0.1) assert(ship.shields > 0.0) ship.shield_regeneration = previous_regeneration ship.restore_shields_from_damage(10.0) assert(ship.shields > 0.0) + captain.tick_skills(10.0) + assert(not captain.is_skill_active("reactor_overload")) + assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0)) captain.tick_skills(25.0) assert(captain.skill_cooldowns["reactor_overload"] == 0.0) diff --git a/tests/captain/reactor_duration_test.gd b/tests/captain/reactor_duration_test.gd new file mode 100644 index 0000000..f107a5a --- /dev/null +++ b/tests/captain/reactor_duration_test.gd @@ -0,0 +1,23 @@ +extends Node +func _ready() -> void: + GameState.start_new_run() + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var captain := GameState.captain + captain.skills = {"reactor_overload": 1, "tactical_maneuver": 1} + captain.equipped_skills = ["reactor_overload", "tactical_maneuver", "", "", ""] + assert(captain.activate_skill("reactor_overload", ship)) + assert(captain.activate_skill("tactical_maneuver", ship)) + assert(captain.skill_durations.reactor_overload == 15.0) + assert(captain.skill_cooldowns.reactor_overload == 30.0) + captain.tick_skills(5.0) + assert(captain.is_skill_active("reactor_overload") and not captain.is_skill_active("tactical_maneuver")) + captain.tick_skills(9.9) + assert(captain.is_skill_active("reactor_overload")) + 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") + get_tree().quit() diff --git a/tests/captain/reactor_duration_test.tscn b/tests/captain/reactor_duration_test.tscn new file mode 100644 index 0000000..f1ba340 --- /dev/null +++ b/tests/captain/reactor_duration_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/captain/reactor_duration_test.gd" id="1"] +[node name="ReactorDurationTest" type="Node"] +script = ExtResource("1") \ No newline at end of file diff --git a/tests/items/enemy_laser_test.gd b/tests/items/enemy_laser_test.gd index 5894d6f..72514ea 100644 --- a/tests/items/enemy_laser_test.gd +++ b/tests/items/enemy_laser_test.gd @@ -18,15 +18,17 @@ func _ready() -> void: enemy.weapon_damage = 1.0 enemy.weapon_cooldown = 1.5 assert(is_equal_approx(enemy.get_fire_interval(), 3.0)) - assert(is_equal_approx(enemy.get_weapon_dps(), 1.0 / 3.0)) + assert(is_equal_approx(enemy.get_shot_damage(), 0.8)) + assert(is_equal_approx(enemy.get_weapon_dps(), 0.8 / 3.0)) + assert(is_equal_approx(enemy.LASER_MISS_CHANCE, 0.45)) enemy.weapon_cooldown = 0.275 assert(enemy.get_fire_interval() >= 1.4) await get_tree().physics_frame await get_tree().physics_frame for index in 100: enemy._fire_plasma(Vector2.LEFT) - var hits := 1000.0 - float(ship.shields) - assert(hits >= 45.0 and hits <= 85.0) + var hits := (1000.0 - float(ship.shields)) / 0.8 + assert(hits >= 35.0 and hits <= 75.0) print("ENEMY_LASER_TEST: PASS (slower cooldown, minimum interval, displayed DPS, %d real hits / 100 shots)" % roundi(hits)) await get_tree().create_timer(0.15).timeout get_tree().quit() diff --git a/tests/levels/wreck_rewards_test.gd.uid b/tests/levels/wreck_rewards_test.gd.uid new file mode 100644 index 0000000..5cf5b8b --- /dev/null +++ b/tests/levels/wreck_rewards_test.gd.uid @@ -0,0 +1 @@ +uid://obqom3fwoh7g