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():
|
if not skill_unavailable_reason(skill, ship).is_empty():
|
||||||
return false
|
return false
|
||||||
skill_cooldowns[skill] = SKILLS.COOLDOWN
|
skill_cooldowns[skill] = SKILLS.COOLDOWN
|
||||||
skill_durations[skill] = SKILLS.DURATION
|
skill_durations[skill] = SKILLS.duration(skill)
|
||||||
if skill == "power_redirect":
|
if skill == "power_redirect":
|
||||||
ship.set("shields", 0.0)
|
ship.set("shields", 0.0)
|
||||||
ship.call("_update_shield_visual")
|
ship.call("_update_shield_visual")
|
||||||
|
|||||||
@ -21,6 +21,9 @@ static func value(skill: String, rank: int) -> float:
|
|||||||
return 0.0
|
return 0.0
|
||||||
return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1])
|
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:
|
static func icon(skill: String) -> Texture2D:
|
||||||
return load("res://assets/icons/captain/%s.png" % skill) as 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":
|
"business_shark":
|
||||||
return "Zakupy za kredyty tańsze o %d%%." % amount
|
return "Zakupy za kredyty tańsze o %d%%." % amount
|
||||||
"reactor_overload":
|
"reactor_overload":
|
||||||
return "Regeneracja energii +%d%% przez 5 sekund." % amount
|
return "Regeneracja energii +%d%% przez 15 sekund." % amount
|
||||||
"tactical_maneuver":
|
"tactical_maneuver":
|
||||||
return "Szansa uniku wynosi %d%% przez 5 sekund. Unik anuluje całe trafienie." % amount
|
return "Szansa uniku wynosi %d%% przez 5 sekund. Unik anuluje całe trafienie." % amount
|
||||||
"efficient_magazine":
|
"efficient_magazine":
|
||||||
|
|||||||
@ -9,7 +9,8 @@ const LOOT_OFFSET := Vector2(0, 36)
|
|||||||
const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd")
|
const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd")
|
||||||
const LASER_COOLDOWN_MULTIPLIER := 2.0
|
const LASER_COOLDOWN_MULTIPLIER := 2.0
|
||||||
const LASER_MINIMUM_COOLDOWN := 1.4
|
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
|
@export_enum("Normal", "Elite", "Boss") var loot_tier: int = 0
|
||||||
|
|
||||||
@ -275,7 +276,7 @@ func _fire_plasma(direction: Vector2) -> void:
|
|||||||
endpoint = hit["position"]
|
endpoint = hit["position"]
|
||||||
var target: Node = hit.get("collider")
|
var target: Node = hit.get("collider")
|
||||||
if target is CharacterBody2D and target.has_method("take_damage"):
|
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()
|
var beam := Line2D.new()
|
||||||
beam.set_script(preload("res://scripts/game/laser_beam_visual.gd"))
|
beam.set_script(preload("res://scripts/game/laser_beam_visual.gd"))
|
||||||
beam.width = 4.0
|
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 * maxf(1.0, distance) + side * offset).normalized()
|
||||||
return direction.rotated(randf_range(-0.02, 0.02))
|
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:
|
func get_weapon_dps() -> float:
|
||||||
var interval := get_fire_interval()
|
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:
|
func _reward_captain() -> void:
|
||||||
if is_instance_valid(_player):
|
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 WORLD_SIZE := Vector2(5200, 3600)
|
||||||
const PLAYER_START := Vector2(180, 200)
|
const PLAYER_START := Vector2(180, 200)
|
||||||
const SAFE_ZONE_RADIUS := 1200.0
|
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 CELESTIAL_TEXTURES := preload("res://scripts/levels/celestial_texture_catalog.gd")
|
||||||
const STAR_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.STAR_TEXTURES
|
const STAR_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.STAR_TEXTURES
|
||||||
const PLANET_TEXTURES: Array[Texture2D] = CELESTIAL_TEXTURES.PLANET_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)
|
ship_rng.seed = seed_value ^ (level * 0x53484950)
|
||||||
var enemy_texture_index := ship_rng.randi_range(0, ALIEN_SHIPS.TEXTURES.size() - 1)
|
var enemy_texture_index := ship_rng.randi_range(0, ALIEN_SHIPS.TEXTURES.size() - 1)
|
||||||
var tiers := BALANCE.enemy_tiers(level)
|
var tiers := BALANCE.enemy_tiers(level)
|
||||||
var cursor := 0
|
# Spread ships across the playable area instead of packing them around planets.
|
||||||
while cursor < tiers.size():
|
for tier in tiers:
|
||||||
var cluster_size := 3 if tiers[cursor] == 2 else rng.randi_range(2, 5)
|
var position := _find_enemy_position(planets, enemies, rng)
|
||||||
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({
|
enemies.append({
|
||||||
"position": position,
|
"position": position,
|
||||||
"tier": tiers[cursor],
|
"tier": tier,
|
||||||
"texture_index": enemy_texture_index,
|
"texture_index": enemy_texture_index,
|
||||||
})
|
})
|
||||||
cursor += 1
|
|
||||||
return {
|
return {
|
||||||
"enemy_texture_index": enemy_texture_index,
|
"enemy_texture_index": enemy_texture_index,
|
||||||
"star_name": star_name,
|
"star_name": star_name,
|
||||||
@ -74,16 +65,29 @@ static func generate(level: int, seed_value: int) -> Dictionary:
|
|||||||
"enemies": enemies,
|
"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:
|
for attempt in 3000:
|
||||||
var candidate: Vector2
|
var candidate := Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580))
|
||||||
if attempt < 500:
|
if not _is_valid_enemy_position(candidate, planets, enemies):
|
||||||
candidate = anchor + Vector2.RIGHT.rotated(rng.randf_range(0, TAU)) * rng.randf_range(130, 420)
|
continue
|
||||||
else:
|
if enemies.is_empty():
|
||||||
candidate = Vector2(rng.randf_range(-2380, 2380), rng.randf_range(-1580, 1580))
|
|
||||||
if _is_valid_enemy_position(candidate, planets, enemies):
|
|
||||||
return candidate
|
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 x in range(-2280, 2281, 130):
|
||||||
for y in range(-1500, 1501, 130):
|
for y in range(-1500, 1501, 130):
|
||||||
var candidate := Vector2(x, y)
|
var candidate := Vector2(x, y)
|
||||||
|
|||||||
@ -205,7 +205,7 @@ func _choice_card(skill: String) -> PanelContainer:
|
|||||||
description.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
description.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
body.add_child(description)
|
body.add_child(description)
|
||||||
if data["active"]:
|
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:
|
else:
|
||||||
body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd")))
|
body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd")))
|
||||||
var select := Button.new()
|
var select := Button.new()
|
||||||
|
|||||||
@ -75,13 +75,17 @@ func _ready() -> void:
|
|||||||
captain.tick_skills(4.9)
|
captain.tick_skills(4.9)
|
||||||
assert(not captain.is_skill_active("power_redirect"))
|
assert(not captain.is_skill_active("power_redirect"))
|
||||||
assert(ship.get_evasion_chance() == 0.0)
|
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))
|
assert(is_equal_approx(captain.skill_cooldowns["reactor_overload"], 24.9))
|
||||||
ship._physics_process(0.1)
|
ship._physics_process(0.1)
|
||||||
assert(ship.shields > 0.0)
|
assert(ship.shields > 0.0)
|
||||||
ship.shield_regeneration = previous_regeneration
|
ship.shield_regeneration = previous_regeneration
|
||||||
ship.restore_shields_from_damage(10.0)
|
ship.restore_shields_from_damage(10.0)
|
||||||
assert(ship.shields > 0.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)
|
captain.tick_skills(25.0)
|
||||||
assert(captain.skill_cooldowns["reactor_overload"] == 0.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_damage = 1.0
|
||||||
enemy.weapon_cooldown = 1.5
|
enemy.weapon_cooldown = 1.5
|
||||||
assert(is_equal_approx(enemy.get_fire_interval(), 3.0))
|
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
|
enemy.weapon_cooldown = 0.275
|
||||||
assert(enemy.get_fire_interval() >= 1.4)
|
assert(enemy.get_fire_interval() >= 1.4)
|
||||||
await get_tree().physics_frame
|
await get_tree().physics_frame
|
||||||
await get_tree().physics_frame
|
await get_tree().physics_frame
|
||||||
for index in 100:
|
for index in 100:
|
||||||
enemy._fire_plasma(Vector2.LEFT)
|
enemy._fire_plasma(Vector2.LEFT)
|
||||||
var hits := 1000.0 - float(ship.shields)
|
var hits := (1000.0 - float(ship.shields)) / 0.8
|
||||||
assert(hits >= 45.0 and hits <= 85.0)
|
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))
|
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
|
await get_tree().create_timer(0.15).timeout
|
||||||
get_tree().quit()
|
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