This commit is contained in:
Tomasz Boruc 2026-10-17 19:46:45 +02:00
parent 5b1d284440
commit a74319d01e
10 changed files with 81 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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