balans
This commit is contained in:
parent
5b1d284440
commit
a74319d01e
@ -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")
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
# 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": tiers[cursor],
|
||||
"tier": tier,
|
||||
"texture_index": enemy_texture_index,
|
||||
})
|
||||
cursor += 1
|
||||
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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
23
tests/captain/reactor_duration_test.gd
Normal file
23
tests/captain/reactor_duration_test.gd
Normal 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()
|
||||
4
tests/captain/reactor_duration_test.tscn
Normal file
4
tests/captain/reactor_duration_test.tscn
Normal 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")
|
||||
@ -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()
|
||||
|
||||
1
tests/levels/wreck_rewards_test.gd.uid
Normal file
1
tests/levels/wreck_rewards_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://obqom3fwoh7g
|
||||
Loading…
x
Reference in New Issue
Block a user