Zmiany w bosach i balans

This commit is contained in:
Tomasz Boruc 2026-10-11 20:24:33 +02:00
parent f14fa23aac
commit 8e90447486
25 changed files with 459 additions and 139 deletions

View File

@ -14,7 +14,7 @@ stat_suffix = " DMG"
weapon_kind = 2 weapon_kind = 2
fire_interval = 0.2 fire_interval = 0.2
weapon_range = 800.0 weapon_range = 800.0
energy_cost = 2.0 energy_cost = 2.5
damage_lower_roll_min = 1 damage_lower_roll_min = 1
damage_lower_roll_max = 3 damage_lower_roll_max = 3
damage_spread_roll_min = 1 damage_spread_roll_min = 1

View File

@ -18,16 +18,16 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
color = Color(0.018, 0.035, 0.065, 1) color = Color(0.018, 0.035, 0.065, 0.97)
[node name="Layout" type="VBoxContainer" parent="."] [node name="Layout" type="VBoxContainer" parent="."]
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = 48.0 offset_left = 28.0
offset_top = 28.0 offset_top = 28.0
offset_right = -48.0 offset_right = -28.0
offset_bottom = -28.0 offset_bottom = -28.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
@ -42,7 +42,7 @@ text = "Wybierz statek"
[node name="Instructions" type="Label" parent="Layout"] [node name="Instructions" type="Label" parent="Layout"]
layout_mode = 2 layout_mode = 2
theme_override_colors/font_color = Color(0.55, 0.7, 0.82, 1) theme_override_colors/font_color = Color(0.55, 0.7, 0.82, 1)
text = "Wybierz jednostkę, którą rozpoczniesz nową grę." text = "Kliknij statek, aby od razu rozpocząć grę."
[node name="ShipScroll" type="ScrollContainer" parent="Layout"] [node name="ShipScroll" type="ScrollContainer" parent="Layout"]
layout_mode = 2 layout_mode = 2
@ -55,22 +55,15 @@ size_flags_horizontal = 3
size_flags_vertical = 0 size_flags_vertical = 0
theme_override_constants/h_separation = 14 theme_override_constants/h_separation = 14
theme_override_constants/v_separation = 14 theme_override_constants/v_separation = 14
columns = 4 columns = 2
[node name="Footer" type="HBoxContainer" parent="Layout"] [node name="Footer" type="HBoxContainer" parent="Layout"]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 20 theme_override_constants/separation = 20
[node name="SelectedLabel" type="Label" parent="Layout/Footer"] [node name="CloseButton" type="Button" parent="Layout/Footer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
vertical_alignment = 1
text = "Wybierz statek"
[node name="ContinueButton" type="Button" parent="Layout/Footer"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(220, 52) custom_minimum_size = Vector2(220, 52)
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 20 theme_override_font_sizes/font_size = 20
text = "Rozpocznij grę" text = "WRÓĆ"

View File

@ -1,30 +1,50 @@
extends Node2D extends Node2D
var enemy: CharacterBody2D var enemy: CharacterBody2D
var _tier_label: Label
func _ready() -> void: func _ready() -> void:
enemy = get_parent() as CharacterBody2D enemy = get_parent() as CharacterBody2D
top_level = true top_level = true
z_index = 30 z_index = 30
light_mask = 4 light_mask = 4
_tier_label = Label.new()
_tier_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tier_label.light_mask = 4
_tier_label.position = Vector2(-50, -23)
_tier_label.size = Vector2(100, 16)
_tier_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_tier_label.add_theme_font_size_override("font_size", 11)
_tier_label.add_theme_color_override("font_outline_color", Color("08111d"))
_tier_label.add_theme_constant_override("outline_size", 3)
add_child(_tier_label)
_update_position() _update_position()
_update_tier_label()
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_update_position() _update_position()
_update_tier_label()
queue_redraw() queue_redraw()
func _update_position() -> void: func _update_position() -> void:
global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0) global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0)
func _tier_color() -> Color:
return Color("ff76d6") if enemy.loot_tier == 2 else (Color("ffc785") if enemy.loot_tier == 1 else Color("ef5967"))
func _update_tier_label() -> void:
_tier_label.text = "BOSS" if enemy.loot_tier == 2 else ("ELITA" if enemy.loot_tier == 1 else "")
_tier_label.add_theme_color_override("font_color", _tier_color())
func get_health_ratio() -> float: func get_health_ratio() -> float:
return clampf(enemy.hull / maxf(enemy.max_hull, 0.001), 0.0, 1.0) return clampf(enemy.hull / maxf(enemy.max_hull, 0.001), 0.0, 1.0)
func _draw() -> void: func _draw() -> void:
if not is_instance_valid(enemy) or enemy.hull <= 0.0: if not is_instance_valid(enemy) or enemy.hull <= 0.0:
return return
var width := 72.0 if enemy.loot_tier == 2 else 44.0 var width := 72.0 if enemy.loot_tier == 2 else (56.0 if enemy.loot_tier == 1 else 44.0)
var background := Rect2(-width * 0.5, -3.0, width, 6.0) var background := Rect2(-width * 0.5, -3.0, width, 6.0)
draw_rect(background.grow(1.0), Color("08111dee")) draw_rect(background.grow(1.0), Color("08111dee"))
draw_rect(background, Color("39232bee")) draw_rect(background, Color("39232bee"))
var color := Color("ffac46") if enemy.loot_tier == 2 else Color("ef5967") var color := _tier_color()
draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color) draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color)

View File

@ -34,13 +34,15 @@ func _ready() -> void:
func apply_level_settings(level: int, tier: int) -> void: func apply_level_settings(level: int, tier: int) -> void:
loot_tier = tier loot_tier = tier
$Sprite2D.modulate = get_tier_tint()
var power := LEVEL_BALANCE.power_scale(level) var power := LEVEL_BALANCE.power_scale(level)
var combat_multiplier := 2.0 if tier == 2 else 1.25
var boss_multiplier := 2.0 if tier == 2 else 1.0 var boss_multiplier := 2.0 if tier == 2 else 1.0
var hull_multiplier := 4.0 if tier == 2 else (2.0 if tier == 1 else 1.0) var hull_multiplier := 4.0 if tier == 2 else (2.0 if tier == 1 else 1.0)
var damage_bonus := 4.0 if tier == 2 else (1.3 if tier == 1 else 1.0) var damage_bonus := 4.0 if tier == 2 else (1.3 if tier == 1 else 1.0)
max_hull = 50.0 * power * hull_multiplier max_hull = 50.0 * power * hull_multiplier * combat_multiplier
hull = max_hull hull = max_hull
weapon_damage = 8.0 * power * damage_bonus weapon_damage = 8.0 * power * damage_bonus * combat_multiplier
approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier
approach_acceleration = 85.0 * boss_multiplier approach_acceleration = 85.0 * boss_multiplier
turn_speed = 1.8 * boss_multiplier turn_speed = 1.8 * boss_multiplier
@ -56,9 +58,12 @@ func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void:
var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height()) var largest_dimension := maxf(ship_texture.get_width(), ship_texture.get_height())
var visual_size := get_visual_size() var visual_size := get_visual_size()
_sprite.scale = Vector2.ONE * (visual_size / largest_dimension) _sprite.scale = Vector2.ONE * (visual_size / largest_dimension)
_sprite.modulate = Color("ffc785") if loot_tier == 1 else Color.WHITE _sprite.modulate = get_tier_tint()
preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite) preload("res://scripts/game/ship_shadow.gd").attach_to(_sprite)
func get_tier_tint() -> Color:
return Color("ffb3df") if loot_tier == 2 else (Color("ffc785") if loot_tier == 1 else Color.WHITE)
func get_visual_size() -> float: func get_visual_size() -> float:
return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0) return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0)

View File

@ -12,6 +12,11 @@ var _ability_bar: HBoxContainer
var _ability_ready_style: StyleBoxFlat var _ability_ready_style: StyleBoxFlat
var _ability_blocked_style: StyleBoxFlat var _ability_blocked_style: StyleBoxFlat
var _inventory_clip: Control var _inventory_clip: Control
var _sidebar: PanelContainer
var _sidebar_toggle: Button
var _sidebar_open := false
var _sidebar_tween: Tween
const SIDEBAR_WIDTH := 344.0
func _ready() -> void: func _ready() -> void:
var energy_indicator := Node2D.new() var energy_indicator := Node2D.new()
@ -25,12 +30,7 @@ func _ready() -> void:
feedback.set("player", player) feedback.set("player", player)
$HUDRoot.add_child(feedback) $HUDRoot.add_child(feedback)
debug_info.add_theme_font_size_override("font_size", 9) debug_info.add_theme_font_size_override("font_size", 9)
# Establish anchor layout before applying offsets; absolute layout resets anchors. _create_sidebar()
var equipment := $HUDRoot/ShipEquipmentPanel as Control
equipment.set("layout_mode", 1)
equipment.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
equipment.offset_left = -330.0
equipment.offset_right = -10.0
_inventory_clip = Control.new() _inventory_clip = Control.new()
_inventory_clip.name = "InventoryDrawer" _inventory_clip.name = "InventoryDrawer"
_inventory_clip.clip_contents = true _inventory_clip.clip_contents = true
@ -45,67 +45,101 @@ func _ready() -> void:
debug_toggle.pressed.connect(_toggle_debug) debug_toggle.pressed.connect(_toggle_debug)
inventory_button.pressed.connect(_toggle_inventory) inventory_button.pressed.connect(_toggle_inventory)
_update_debug_toggle() _update_debug_toggle()
$HUDRoot/CaptainPanel.resized.connect(func() -> void: _layout_sidebar.call_deferred())
_layout_sidebar.call_deferred() _layout_sidebar.call_deferred()
func _create_sidebar() -> void:
_sidebar = PanelContainer.new()
_sidebar.name = "CaptainShipDrawer"
var style := StyleBoxFlat.new()
style.bg_color = Color(0.018, 0.028, 0.043, 0.97)
style.border_color = Color("52677e")
style.set_border_width_all(1)
style.set_content_margin_all(8)
_sidebar.add_theme_stylebox_override("panel", style)
$HUDRoot.add_child(_sidebar)
var scroll := ScrollContainer.new()
scroll.name = "Scroll"
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
_sidebar.add_child(scroll)
var contents := VBoxContainer.new()
contents.name = "Contents"
contents.size_flags_horizontal = Control.SIZE_EXPAND_FILL
contents.add_theme_constant_override("separation", 8)
scroll.add_child(contents)
var captain := $HUDRoot/CaptainPanel as PanelContainer
var equipment := $HUDRoot/ShipEquipmentPanel as PanelContainer
for panel in [captain, equipment]:
panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
panel.reparent(contents, false)
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
if panel == captain:
contents.add_child(HSeparator.new())
_sidebar_toggle = Button.new()
_sidebar_toggle.name = "CaptainShipToggle"
_sidebar_toggle.text = "KAPITAN / STATEK ◀"
_sidebar_toggle.add_theme_font_size_override("font_size", 12)
_sidebar_toggle.focus_mode = Control.FOCUS_NONE
$HUDRoot.add_child(_sidebar_toggle)
_sidebar_toggle.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
_sidebar_toggle.offset_left = -210
_sidebar_toggle.offset_right = -8
_sidebar_toggle.offset_top = 8
_sidebar_toggle.offset_bottom = 38
_sidebar_toggle.pressed.connect(_toggle_sidebar)
_sidebar.hide()
func _toggle_sidebar() -> void:
_sidebar_open = not _sidebar_open
if _sidebar_tween != null and _sidebar_tween.is_running():
_sidebar_tween.kill()
_sidebar_toggle.text = "ZAMKNIJ ▶" if _sidebar_open else "KAPITAN / STATEK ◀"
if _sidebar_open:
_sidebar.show()
else:
close_inventory()
_sidebar_tween = create_tween()
_sidebar_tween.tween_property(_sidebar, "position:x", $HUDRoot.size.x - SIDEBAR_WIDTH - 8 if _sidebar_open else $HUDRoot.size.x, 0.25).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT if _sidebar_open else Tween.EASE_IN)
if not _sidebar_open:
_sidebar_tween.tween_callback(_sidebar.hide)
func _layout_sidebar() -> void: func _layout_sidebar() -> void:
var captain_panel := $HUDRoot/CaptainPanel as Control if _sidebar_tween != null and _sidebar_tween.is_running():
var equipment_panel := $HUDRoot/ShipEquipmentPanel as Control _sidebar_tween.kill()
captain_panel.grow_vertical = Control.GROW_DIRECTION_END _sidebar.size = Vector2(SIDEBAR_WIDTH, maxf(100.0, $HUDRoot.size.y - 52.0))
equipment_panel.grow_vertical = Control.GROW_DIRECTION_END _sidebar.position = Vector2($HUDRoot.size.x - SIDEBAR_WIDTH - 8 if _sidebar_open else $HUDRoot.size.x, 44)
equipment_panel.position.y = captain_panel.position.y + captain_panel.size.y + 6.0 _sidebar.visible = _sidebar_open
equipment_panel.size.y = equipment_panel.get_combined_minimum_size().y
_inventory_clip.size = inventory_panel.size _inventory_clip.size = inventory_panel.size
_inventory_clip.position = Vector2(equipment_panel.position.x - _inventory_clip.size.x - 8, equipment_panel.position.y) _layout_inventory()
func _layout_inventory() -> void:
if _inventory_clip == null:
return
var equipment := _sidebar.get_node("Scroll/Contents/ShipEquipmentPanel") as Control
_inventory_clip.position = Vector2(_sidebar.position.x - _inventory_clip.size.x - 8, minf(equipment.global_position.y, maxf(8.0, $HUDRoot.size.y - inventory_panel.size.y - 8)))
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_layout_inventory()
_update_ability_bar() _update_ability_bar()
if not _debug_expanded: if not _debug_expanded:
return return
var ship_position := player.global_position
var ship_velocity := player.velocity
var angular_speed := float(player.get("angular_velocity"))
var game := get_parent() var game := get_parent()
var enemy_count: int = game.call("get_remaining_enemies") debug_info.text = """POZIOM UKŁADU: %d
var goal: Node2D = game.get("protected_planet")
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 · ŚPM: rakieta
1–6: aktywne rozszerzenia
UKŁAD: %d · PLANETY: %d
WROGOWIE: %d WROGOWIE: %d
CEL: %s
PLANETA RASY: %.0f px · %.0f°
STATEK
KADŁUB: %d / %d
TARCZE: %d / %d
ENERGIA: %d / %d
REGEN E / T: %.1f / %.1f /s
RUCH
PRĘDKOŚĆ: %.1f px/s PRĘDKOŚĆ: %.1f px/s
WEKTOR: [%.1f, %.1f] px/s
POZYCJA: [%.1f, %.1f] AKTUALNA REGENERACJA
KIERUNEK: %.1f° Energia: %.1f pkt/s
OBRÓT: %.2f rad/s""" % [ Tarcze: %.1f pkt/s
GameState.system_level, int(game.get("system_data")["planets"].size()), enemy_count,
"Odwiedź chronioną rasę" if bool(game.get("system_cleared")) else "Oczyść układ", %s""" % [
to_planet.length(), wrapf(rad_to_deg(to_planet.angle()) + 90.0, 0.0, 360.0), GameState.system_level,
roundi(float(player.get("hull"))), roundi(float(player.get("max_hull"))), int(game.call("get_remaining_enemies")),
roundi(float(player.get("shields"))), roundi(float(player.get("max_shields"))), player.velocity.length(),
roundi(float(player.get("energy"))), roundi(float(player.get("max_energy"))),
float(player.get("energy_regeneration")) + float(player.call("get_solar_regeneration")), float(player.get("energy_regeneration")) + float(player.call("get_solar_regeneration")),
float(player.get("shield_regeneration")), float(player.get("shield_regeneration")),
ship_velocity.length(), ship_velocity.x, ship_velocity.y, _get_bonus_summary(),
ship_position.x, ship_position.y, rad_to_deg(player.rotation), angular_speed,
] ]
debug_info.text += "\n\n" + _get_bonus_summary()
func _toggle_debug() -> void: func _toggle_debug() -> void:
_debug_expanded = not _debug_expanded _debug_expanded = not _debug_expanded
@ -116,47 +150,48 @@ func _update_debug_toggle() -> void:
debug_toggle.text = "DEBUG [−]" if _debug_expanded else "DEBUG [+]" debug_toggle.text = "DEBUG [−]" if _debug_expanded else "DEBUG [+]"
func _toggle_inventory() -> void: func _toggle_inventory() -> void:
if not _sidebar_open:
_toggle_sidebar()
inventory_panel.call("toggle_drawer") inventory_panel.call("toggle_drawer")
func close_inventory() -> void: func close_inventory() -> void:
inventory_panel.call("close_drawer", true) inventory_panel.call("close_drawer", true)
func _get_bonus_summary() -> String: func _get_bonus_summary() -> String:
# Read final ship values so gear and captain multipliers match actual combat. var cooldown_bonus := 0.0
return """BONUSY ŁĄCZNE (MODUŁY + KAPITAN) var inventory := player.get_node("ShipInventory") as ShipInventory
Obrażenia broni: %+.1f%% (×%.3f) for item in inventory.weapon_slots + inventory.module_slots:
Szansa na krytyk: %.1f%% if item != null:
Obrażenia krytyczne: ×%.2f (%+.1f%% bonusu) cooldown_bonus += item.get_stat_total("missile_cooldown_reduction")
Szansa na unik: %.1f%% return """BONUSY (ADDONY + KAPITAN)
Redukcja obrażeń: %.1f%% Obrażenia broni: %+.1f%%
Szansa na krytyk: %+.1f%%
Regeneracja energii: %.1f pkt/s (%+.1f) Obrażenia krytyczne: %+.1f%%
Ładowanie solarne: +%.1f pkt/s Szansa na unik: %+.1f%%
Regeneracja tarcz: %.1f pkt/s (%+.1f) Redukcja obrażeń: %+.1f%%
Odzysk tarcz z obrażeń: %.1f%% Odzysk tarcz z obrażeń: %.1f%%
Maks. kadłub: %.1f (%+.1f pkt) Regeneracja energii: %+.1f pkt/s
Maks. tarcze: %.1f (%+.1f pkt) Regeneracja tarcz: %+.1f pkt/s
Maks. energia: %.1f (%+.1f pkt) Kadłub: %+.1f pkt
Maks. prędkość: %.1f px/s (%+.1f%%) Tarcze: %+.1f pkt
Zdobywane XP: %+.1f%% Energia: %+.1f pkt
Zdobywane kredyty: %+.1f%%""" % [ Prędkość maks.: %+.1f%%
Skrócenie cooldownu rakiet: %.2f s
Doświadczenie: %+.1f%%
Kredyty: %+.1f%%""" % [
(float(player.call("get_weapon_damage_multiplier")) - 1.0) * 100.0, (float(player.call("get_weapon_damage_multiplier")) - 1.0) * 100.0,
float(player.call("get_weapon_damage_multiplier")),
float(player.call("get_critical_chance")) * 100.0, float(player.call("get_critical_chance")) * 100.0,
float(player.call("get_critical_multiplier")),
float(player.get("critical_damage_bonus")) * 100.0, float(player.get("critical_damage_bonus")) * 100.0,
float(player.call("get_evasion_chance")) * 100.0, float(player.call("get_evasion_chance")) * 100.0,
float(player.call("get_damage_reduction")) * 100.0, float(player.call("get_damage_reduction")) * 100.0,
float(player.get("energy_regeneration")),
float(player.get("energy_regeneration")) - float(player.get("base_energy_regeneration")),
float(player.call("get_solar_regeneration")),
float(player.get("shield_regeneration")),
float(player.get("shield_regeneration")) - float(player.get("base_shield_regeneration")),
float(player.get("shield_on_damage_percent")), float(player.get("shield_on_damage_percent")),
float(player.get("max_hull")), float(player.get("max_hull")) - float(player.get("base_max_hull")), float(player.get("energy_regeneration")) - float(player.get("base_energy_regeneration")),
float(player.get("max_shields")), float(player.get("max_shields")) - float(player.get("base_max_shields")), float(player.get("shield_regeneration")) - float(player.get("base_shield_regeneration")),
float(player.get("max_energy")), float(player.get("max_energy")) - float(player.get("base_max_energy")), float(player.get("max_hull")) - float(player.get("base_max_hull")),
float(player.get("max_speed")), (float(player.get("max_speed")) / float(player.get("base_max_speed")) - 1.0) * 100.0, float(player.get("max_shields")) - float(player.get("base_max_shields")),
float(player.get("max_energy")) - float(player.get("base_max_energy")),
(float(player.get("max_speed")) / float(player.get("base_max_speed")) - 1.0) * 100.0,
cooldown_bonus,
(float(player.get("experience_multiplier")) - 1.0) * 100.0, (float(player.get("experience_multiplier")) - 1.0) * 100.0,
(float(player.get("credits_multiplier")) - 1.0) * 100.0, (float(player.get("credits_multiplier")) - 1.0) * 100.0,
] ]

View File

@ -2,8 +2,7 @@ extends RefCounted
const MAX_LEVEL := 1000 const MAX_LEVEL := 1000
const COUNT_POINTS := [Vector2(1, 5), Vector2(10, 10), Vector2(50, 18), Vector2(100, 23), Vector2(250, 29), Vector2(500, 34), Vector2(1000, 40)] const COUNT_POINTS := [Vector2(1, 5), Vector2(10, 10), Vector2(50, 18), Vector2(100, 23), Vector2(250, 29), Vector2(500, 34), Vector2(1000, 40)]
# x = level, y = elite fraction. Each level has exactly one boss. const ELITES_PER_LEVEL := 2
const TIER_POINTS := [Vector2(10, 0), Vector2(50, 0.15), Vector2(100, 0.25), Vector2(500, 0.45), Vector2(1000, 0.55)]
static func enemy_count(level: int) -> int: static func enemy_count(level: int) -> int:
var clamped_level := clampi(level, 1, MAX_LEVEL) var clamped_level := clampi(level, 1, MAX_LEVEL)
@ -16,19 +15,9 @@ static func enemy_count(level: int) -> int:
static func enemy_tiers(level: int) -> Array[int]: static func enemy_tiers(level: int) -> Array[int]:
var count := enemy_count(level) var count := enemy_count(level)
var elite_fraction := 0.0 # One boss and two elites on every level, included in the total count.
for index in range(1, TIER_POINTS.size()):
var before: Vector2 = TIER_POINTS[index - 1]
var after: Vector2 = TIER_POINTS[index]
if level <= after.x:
var progress := clampf(inverse_lerp(before.x, after.x, level), 0.0, 1.0)
elite_fraction = lerpf(before.y, after.y, progress)
break
if level >= MAX_LEVEL:
elite_fraction = 0.55
# One boss on every level, included in the total enemy count.
var bosses := 1 var bosses := 1
var elites := mini(count - bosses, roundi(count * elite_fraction)) var elites := ELITES_PER_LEVEL
var tiers: Array[int] = [2] var tiers: Array[int] = [2]
for elite in elites: for elite in elites:
tiers.append(1) tiers.append(1)

View File

@ -1,16 +1,65 @@
extends Control extends Control
const SHIP_SELECTION_SCENE := "res://scenes/ui/ship_selection.tscn" const SHIP_SELECTION_SCENE := preload("res://scenes/ui/ship_selection.tscn")
const GAME_SCENE := "res://scenes/game/game.tscn"
@onready var start_button: Button = %StartButton @onready var start_button: Button = %StartButton
@onready var exit_button: Button = %ExitButton @onready var exit_button: Button = %ExitButton
var _ship_panel: Control
var _panel_open := false
var _launching := false
var _drawer_tween: Tween
func _ready() -> void: func _ready() -> void:
start_button.pressed.connect(_start_game) start_button.pressed.connect(_start_game)
exit_button.pressed.connect(_exit_game) exit_button.pressed.connect(_exit_game)
_ship_panel = SHIP_SELECTION_SCENE.instantiate() as Control
_ship_panel.name = "ShipSelectionPanel"
_ship_panel.hide()
add_child(_ship_panel)
_ship_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
_ship_panel.connect("ship_selected", _launch_game)
_ship_panel.connect("close_requested", _close_ship_panel)
resized.connect(_layout_ship_panel)
_layout_ship_panel()
func _start_game() -> void: func _start_game() -> void:
get_tree().change_scene_to_file(SHIP_SELECTION_SCENE) if _panel_open or _launching:
return
if _drawer_tween != null and _drawer_tween.is_running():
_drawer_tween.kill()
_panel_open = true
start_button.disabled = true
_ship_panel.position.x = size.x
_ship_panel.show()
_drawer_tween = create_tween()
_drawer_tween.tween_property(_ship_panel, "position:x", size.x - _ship_panel.size.x, 0.3).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
func _layout_ship_panel() -> void:
if _ship_panel == null:
return
if _drawer_tween != null and _drawer_tween.is_running():
_drawer_tween.kill()
_ship_panel.size = Vector2(minf(700.0, size.x * 0.55), size.y)
_ship_panel.position = Vector2(size.x - _ship_panel.size.x if _panel_open else size.x, 0)
func _close_ship_panel() -> void:
if not _panel_open or _launching:
return
_panel_open = false
start_button.disabled = false
if _drawer_tween != null and _drawer_tween.is_running():
_drawer_tween.kill()
_drawer_tween = create_tween()
_drawer_tween.tween_property(_ship_panel, "position:x", size.x, 0.2).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
_drawer_tween.tween_callback(_ship_panel.hide)
func _launch_game() -> void:
if _launching:
return
_launching = true
GameState.start_new_run()
get_tree().change_scene_to_file(GAME_SCENE)
func _exit_game() -> void: func _exit_game() -> void:
get_tree().quit() get_tree().quit()

View File

@ -67,7 +67,8 @@ func _position_at_planet() -> void:
var planet_radius := float(planet.get("radius")) * canvas.x.length() var planet_radius := float(planet.get("radius")) * canvas.x.length()
size = panel.get_combined_minimum_size() size = panel.get_combined_minimum_size()
var viewport_size := get_viewport_rect().size var viewport_size := get_viewport_rect().size
var screen_right := viewport_size.x - 320.0 var sidebar := get_parent().get_node_or_null("CaptainShipDrawer") as Control
var screen_right := viewport_size.x - (sidebar.size.x + 8.0 if sidebar != null and sidebar.visible else 8.0)
var target := planet_screen + Vector2(planet_radius + 24.0, -size.y * 0.5) var target := planet_screen + Vector2(planet_radius + 24.0, -size.y * 0.5)
if target.x + size.x > screen_right: if target.x + size.x > screen_right:
target.x = planet_screen.x - planet_radius - 24.0 - size.x target.x = planet_screen.x - planet_radius - 24.0 - size.x

View File

@ -1,17 +1,17 @@
extends Control extends Control
const GAME_SCENE := "res://scenes/game/game.tscn" signal ship_selected
signal close_requested
const CATALOG := preload("res://scripts/game/ship_catalog.gd") const CATALOG := preload("res://scripts/game/ship_catalog.gd")
@onready var ship_grid: GridContainer = %ShipGrid @onready var ship_grid: GridContainer = %ShipGrid
@onready var selected_label: Label = %SelectedLabel @onready var close_button: Button = %CloseButton
@onready var continue_button: Button = %ContinueButton
var button_group := ButtonGroup.new() var button_group := ButtonGroup.new()
func _ready() -> void: func _ready() -> void:
button_group.allow_unpress = false button_group.allow_unpress = false
continue_button.pressed.connect(_start_game) close_button.pressed.connect(func() -> void: close_requested.emit())
_load_ships() _load_ships()
func _load_ships() -> void: func _load_ships() -> void:
@ -23,25 +23,35 @@ func _load_ships() -> void:
continue continue
var button := Button.new() var button := Button.new()
button.name = "Ship%d" % int(chassis["number"]) button.name = "Ship%d" % int(chassis["number"])
button.custom_minimum_size = Vector2(250, 260) button.custom_minimum_size = Vector2(250, 240)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.toggle_mode = true button.toggle_mode = true
button.button_group = button_group button.button_group = button_group
var contents := VBoxContainer.new()
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(contents)
contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
contents.offset_left = 12
contents.offset_right = -12
contents.offset_top = 12
contents.offset_bottom = -12
contents.add_theme_constant_override("separation", 12)
var preview := TextureRect.new() var preview := TextureRect.new()
preview.custom_minimum_size.y = 100
preview.texture = texture preview.texture = texture
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
button.add_child(preview) contents.add_child(preview)
preview.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) var description := Label.new()
preview.offset_left = -50.0 description.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.offset_right = 50.0 description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
preview.offset_top = 12.0 description.text = "%s\n%s\n\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)]
preview.offset_bottom = 102.0 description.add_theme_font_size_override("font_size", 13)
button.text = "%s\n%s\n\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)] contents.add_child(description)
button.add_theme_font_size_override("font_size", 13) button.add_theme_font_size_override("font_size", 13)
button.tooltip_text = "%s — %s\nStatystyki bazowe przed bonusami modułów.\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)] button.tooltip_text = "%s — %s\nStatystyki bazowe przed bonusami modułów.\n%s" % [chassis["name"], chassis["role"], CATALOG.stats_text(chassis)]
button.pressed.connect(_select_ship.bind(chassis, button)) button.pressed.connect(_choose_ship.bind(chassis, button))
ship_grid.add_child(button) ship_grid.add_child(button)
if chassis["number"] == selected_ship["number"]: if chassis["number"] == selected_ship["number"]:
_select_ship(chassis, button) _select_ship(chassis, button)
@ -49,9 +59,12 @@ func _load_ships() -> void:
func _select_ship(chassis: Dictionary, button: Button) -> void: func _select_ship(chassis: Dictionary, button: Button) -> void:
GameState.selected_ship_path = CATALOG.texture_path(chassis) GameState.selected_ship_path = CATALOG.texture_path(chassis)
button.button_pressed = true button.button_pressed = true
selected_label.text = "Wybrano: %s · %s" % [chassis["name"], chassis["role"]]
continue_button.disabled = false
func _start_game() -> void: func _choose_ship(chassis: Dictionary, button: Button) -> void:
GameState.start_new_run() _select_ship(chassis, button)
get_tree().change_scene_to_file(GAME_SCENE) ship_selected.emit()
func _unhandled_input(event: InputEvent) -> void:
if visible and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
close_requested.emit()
get_viewport().set_input_as_handled()

View File

@ -54,7 +54,7 @@ func _ready() -> void:
assert(profile.credits == 0) assert(profile.credits == 0)
await _collect_credits(game.get_node("PlayerShip") as CharacterBody2D) await _collect_credits(game.get_node("PlayerShip") as CharacterBody2D)
assert(profile.credits == 180) assert(profile.credits == 180)
var panel := game.get_node("HUD/HUDRoot/CaptainPanel") var panel := game.get_node("HUD/HUDRoot/CaptainShipDrawer/Scroll/Contents/CaptainPanel")
panel.get_node("Contents/Skills/gunner/Train_gunner").emit_signal("pressed") panel.get_node("Contents/Skills/gunner/Train_gunner").emit_signal("pressed")
assert(profile.get_skill_rank("gunner") == 1 and profile.learning_points == 0) assert(profile.get_skill_rank("gunner") == 1 and profile.learning_points == 0)
panel.get_node("Contents/Skills/piloting/Train_piloting").emit_signal("pressed") panel.get_node("Contents/Skills/piloting/Train_piloting").emit_signal("pressed")

View File

@ -0,0 +1,52 @@
extends Node2D
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const REGEN := preload("res://resources/items/affixes/energy_regeneration.tres")
class MeasuredLaser:
extends "res://scripts/game/weapon_controller.gd"
var shots := 0
func _consume_energy(amount: float) -> bool:
var consumed := super._consume_energy(amount)
if consumed:
shots += 1
return consumed
func _ready() -> void:
seed(54321)
GameState.start_new_run()
var ship := preload("res://scenes/game/player_ship.tscn").instantiate() as CharacterBody2D
add_child(ship)
ship.get_node("WeaponController").set_physics_process(false)
var inventory := ship.get_node("ShipInventory") as ShipInventory
var laser := GENERATOR.generate_for_definition(preload("res://resources/items/beam_laser.tres"), 5)
assert(inventory.add_item(laser))
assert(inventory.move_item("cargo", inventory.cargo.find(laser), "weapon", 0))
# Five separate level-5 regeneration affixes, on five ordinary hull modules.
for index in 5:
var module := ItemInstance.new()
module.definition = preload("res://resources/items/hull_plating.tres")
module.item_level = 5
var addon := ItemStatModifier.new()
addon.affix = REGEN
addon.stat_key = REGEN.stat_key
addon.value = REGEN.roll_value(5)
module.additional_modifiers.append(addon)
assert(inventory.add_item(module))
assert(inventory.move_item("cargo", inventory.cargo.find(module), "module", index))
print("REGEN_BONUS_%d: %.4f energy/s" % [index + 1, module.get_stat_total("energy_regeneration")])
var controller := MeasuredLaser.new()
ship.add_child(controller)
await get_tree().physics_frame
ship.set("energy", 80.0)
assert(ship.call("get_solar_regeneration") == 0.0)
print("LASER_SETUP: level=5 DMG=%s addons=%d cost=%.2f regen=%.4f" % [laser.get_damage_range_text(), laser.get_addon_count(), laser.get_energy_cost(), ship.get("energy_regeneration")])
Input.action_press("ship_fire_primary")
for frame in 600:
await get_tree().physics_frame
Input.action_release("ship_fire_primary")
var final_energy := float(ship.get("energy"))
assert(controller.shots > 0)
assert(final_energy < 80.0)
print("LASER_ENERGY_TEST: PASS duration=10s shots=%d initial=80 final=%.4f loss=%.4f" % [controller.shots, final_energy, 80.0 - final_energy])
get_tree().quit()

View File

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

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/items/laser_energy_regeneration_test.gd" id="1"]
[node name="LaserEnergyRegenerationTest" type="Node2D"]
script = ExtResource("1")

View File

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

View File

@ -0,0 +1,34 @@
extends Node
const BALANCE := preload("res://scripts/levels/level_balance.gd")
const GENERATOR := preload("res://scripts/levels/system_generator.gd")
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
func _ready() -> void:
for level in range(1, 1001):
var tiers := BALANCE.enemy_tiers(level)
assert(tiers.count(1) == 2 and tiers.count(2) == 1)
assert(tiers.size() == BALANCE.enemy_count(level))
assert(tiers.count(0) >= 2)
for level in [1, 10, 100, 1000]:
var data: Dictionary = GENERATOR.generate(level, 314159)
var elites := 0
var bosses := 0
for entry in data["enemies"]:
elites += 1 if entry["tier"] == 1 else 0
bosses += 1 if entry["tier"] == 2 else 0
assert(elites == 2 and bosses == 1)
for tier in 3:
var enemy := ENEMY.instantiate() as CharacterBody2D
add_child(enemy)
enemy.set_physics_process(false)
enemy.call("apply_level_settings", 1, tier)
var bar := enemy.get_node("HealthBar")
bar.call("_process", 0.0)
var label := bar.get_child(0) as Label
assert(label.text == ["", "ELITA", "BOSS"][tier])
assert(label.light_mask == 4)
assert(enemy.get_node("Sprite2D").modulate == enemy.call("get_tier_tint"))
enemy.queue_free()
print("ENEMY_TIERS_TEST: PASS (1000 levels, generated encounters, exactly two elites/one boss, tint and labels)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/levels/enemy_tiers_test.gd" id="1"]
[node name="EnemyTiersTest" type="Node"]
script = ExtResource("1")

View File

@ -127,7 +127,7 @@ func _ready() -> void:
_assert_full_resources(player) _assert_full_resources(player)
var enemy := get_tree().get_nodes_in_group("enemies")[0] as CharacterBody2D var enemy := get_tree().get_nodes_in_group("enemies")[0] as CharacterBody2D
enemy.call("apply_level_settings", 2, 2) enemy.call("apply_level_settings", 2, 2)
assert(is_equal_approx(float(enemy.get("max_hull")), 210.0)) assert(is_equal_approx(float(enemy.get("max_hull")), 420.0))
enemy.call("take_damage", 100000.0) enemy.call("take_damage", 100000.0)
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
@ -193,7 +193,7 @@ func _assert_boss_and_health(game: Node2D) -> void:
normal = enemy normal = enemy
assert(boss != null and normal != null) assert(boss != null and normal != null)
for key in ["max_hull", "weapon_damage"]: for key in ["max_hull", "weapon_damage"]:
assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 4.0)) assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 6.4))
for key in ["approach_speed", "approach_acceleration", "turn_speed", "activation_range", "weapon_range"]: for key in ["approach_speed", "approach_acceleration", "turn_speed", "activation_range", "weapon_range"]:
assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 2.0)) assert(is_equal_approx(float(boss.get(key)), float(normal.get(key)) * 2.0))
assert(is_equal_approx(boss.weapon_cooldown, normal.weapon_cooldown * 0.5)) assert(is_equal_approx(boss.weapon_cooldown, normal.weapon_cooldown * 0.5))

View File

@ -0,0 +1,60 @@
extends Node
const GAME := preload("res://scenes/game/game.tscn")
func _ready() -> void:
get_tree().create_timer(10.0).timeout.connect(func() -> void:
push_error("CAPTAIN_SHIP_DRAWER_TEST: timed out")
get_tree().quit(1)
)
GameState.start_new_run()
var game := GAME.instantiate() as Node2D
add_child(game)
await get_tree().process_frame
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.set_physics_process(false)
var ship := game.get_node("PlayerShip") as CharacterBody2D
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
var hud := game.get_node("HUD")
var root := hud.get_node("HUDRoot") as Control
var drawer := root.get_node("CaptainShipDrawer") as Control
var toggle := root.get_node("CaptainShipToggle") as Button
var captain := drawer.get_node("Scroll/Contents/CaptainPanel")
var equipment := drawer.get_node("Scroll/Contents/ShipEquipmentPanel")
assert(captain.get_parent() == equipment.get_parent())
assert(not drawer.visible)
toggle.pressed.emit()
await get_tree().create_timer(0.3).timeout
assert(drawer.visible and is_equal_approx(drawer.position.x + drawer.size.x, root.size.x - 8.0))
GameState.captain.learning_points = 1
GameState.captain.emit_changed()
captain.get_node("Contents/Skills/gunner/Train_gunner").pressed.emit()
assert(GameState.captain.get_skill_rank("gunner") == 1)
equipment.get_node("Contents/InventoryButton").pressed.emit()
await get_tree().create_timer(0.3).timeout
var inventory_panel: Control = hud.get("inventory_panel")
assert(inventory_panel.visible and inventory_panel.get("_drawer_open"))
var inventory := ship.get_node("ShipInventory") as ShipInventory
var item := ItemInstance.new()
item.definition = preload("res://resources/items/engine_extension.tres")
assert(inventory.add_item(item))
assert(inventory.move_item("cargo", inventory.cargo.find(item), "module", 0))
assert(ship.get("max_speed") > ship.get("base_max_speed"))
toggle.pressed.emit()
await get_tree().create_timer(0.3).timeout
assert(not drawer.visible and not inventory_panel.visible)
toggle.pressed.emit()
await get_tree().create_timer(0.3).timeout
assert(drawer.visible and not inventory_panel.visible)
var original_size := root.size
root.size = Vector2(root.size.x, 480)
hud.call("_layout_sidebar")
await get_tree().process_frame
await get_tree().process_frame
var scroll := drawer.get_node("Scroll") as ScrollContainer
assert(scroll.get_v_scroll_bar().visible)
root.size = original_size
hud.call("_layout_sidebar")
print("CAPTAIN_SHIP_DRAWER_TEST: PASS (merged panel, slide, close/reopen, skills, inventory, equipment stats)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/ships/captain_ship_drawer_test.gd" id="1"]
[node name="CaptainShipDrawerTest" type="Node"]
script = ExtResource("1")

View File

@ -0,0 +1,43 @@
extends Node
const MENU := preload("res://scenes/ui/main_menu.tscn")
const CATALOG := preload("res://scripts/game/ship_catalog.gd")
func _ready() -> void:
get_tree().create_timer(10.0).timeout.connect(func() -> void:
push_error("SHIP_SELECTION_DRAWER_TEST: timed out")
get_tree().quit(1)
)
var menu := MENU.instantiate() as Control
get_tree().root.add_child.call_deferred(menu)
await get_tree().process_frame
await get_tree().process_frame
get_tree().current_scene = menu
var panel := menu.get_node("ShipSelectionPanel") as Control
assert(not panel.visible)
var start := menu.get_node("MenuLayout/StartButton") as Button
start.pressed.emit()
assert(get_tree().current_scene == menu and panel.visible)
assert(is_equal_approx(panel.position.x, menu.size.x))
await get_tree().create_timer(0.35).timeout
assert(is_equal_approx(panel.position.x + panel.size.x, menu.size.x))
var grid := panel.get_node("Layout/ShipScroll/ShipGrid") as GridContainer
assert(grid.columns == 2 and grid.get_child_count() == 4)
assert(not panel.has_node("Layout/Footer/ContinueButton"))
panel.get_node("Layout/Footer/CloseButton").pressed.emit()
await get_tree().create_timer(0.25).timeout
assert(not panel.visible and not start.disabled)
start.pressed.emit()
await get_tree().create_timer(0.35).timeout
GameState.captain.credits = 999
grid.get_node("Ship7").pressed.emit()
assert(GameState.selected_ship_path == CATALOG.texture_path(CATALOG.SHIPS[3]))
await get_tree().process_frame
await get_tree().process_frame
await get_tree().process_frame
var game := get_tree().current_scene
assert(game.scene_file_path == "res://scenes/game/game.tscn")
assert(GameState.captain.credits == 0 and GameState.system_level == 1)
assert(game.get_node("PlayerShip").get("max_energy") == 125.0)
print("SHIP_SELECTION_DRAWER_TEST: PASS (Start opens drawer in menu, animation, four ships, close/reopen, one-click launch, selected ship stats, fresh run)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/ships/ship_selection_drawer_test.gd" id="1"]
[node name="ShipSelectionDrawerTest" type="Node"]
script = ExtResource("1")

View File

@ -23,7 +23,10 @@ func _ready() -> void:
assert(ship.get("max_speed") == chassis["speed"] and ship.get("energy_regeneration") == 5.0) assert(ship.get("max_speed") == chassis["speed"] and ship.get("energy_regeneration") == 5.0)
assert(ship.get_node("Sprite2D").texture.resource_path == CATALOG.texture_path(chassis)) assert(ship.get_node("Sprite2D").texture.resource_path == CATALOG.texture_path(chassis))
var inventory: ShipInventory = ship.get_node("ShipInventory") var inventory: ShipInventory = ship.get_node("ShipInventory")
assert(inventory.move_item("cargo", 2, "module", 0)) var shield := ItemInstance.new()
shield.definition = preload("res://resources/items/reinforced_shield.tres")
assert(inventory.add_item(shield))
assert(inventory.move_item("cargo", inventory.cargo.find(shield), "module", 0))
var equipped := inventory.get_item("module", 0) var equipped := inventory.get_item("module", 0)
assert(ship.get("max_shields") == chassis["shields"] + equipped.get_stat_total("max_shields")) assert(ship.get("max_shields") == chassis["shields"] + equipped.get_stat_total("max_shields"))
inventory.preserve_for_restart() inventory.preserve_for_restart()