Zmiany w skilach

This commit is contained in:
Tomasz Boruc 2026-10-11 17:16:45 +02:00
parent 2a57f0face
commit 897123c7c8
16 changed files with 301 additions and 47 deletions

View File

@ -71,6 +71,13 @@ theme_override_font_sizes/font_size = 12
focus_mode = 0 focus_mode = 0
text = "KUP 100 AMUNICJI — 100 KREDYTÓW" text = "KUP 100 AMUNICJI — 100 KREDYTÓW"
[node name="ResetSkillsButton" type="Button" parent="Center/Panel/Contents"]
custom_minimum_size = Vector2(0, 36)
layout_mode = 2
theme_override_font_sizes/font_size = 12
focus_mode = 0
text = "RESET SKILLI — 100 KREDYTÓW"
[node name="NextButton" type="Button" parent="Center/Panel/Contents"] [node name="NextButton" type="Button" parent="Center/Panel/Contents"]
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2

View File

@ -3,7 +3,8 @@ extends RefCounted
const XP_BASE := 100 const XP_BASE := 100
const XP_PER_LEVEL := 50 const XP_PER_LEVEL := 50
const LEARNING_POINTS_PER_LEVEL := 1 const LEARNING_POINTS_PER_LEVEL := 1
const MAX_SKILL_RANK := 100 const MAX_SKILL_RANK := 20
const SKILL_RESET_COST_PER_LEVEL := 100
const BASE_CRITICAL_MULTIPLIER := 2.0 const BASE_CRITICAL_MULTIPLIER := 2.0
const KILL_XP := [20, 50, 150] const KILL_XP := [20, 50, 150]
const KILL_CREDITS := [15, 40, 120] const KILL_CREDITS := [15, 40, 120]
@ -12,11 +13,24 @@ const SKILL_NAMES := {
"mechanic": "Mechanik", "sharp_eye": "Celne oko", "mechanic": "Mechanik", "sharp_eye": "Celne oko",
} }
const SKILL_HELP := { const SKILL_HELP := {
"piloting": "Każdy poziom: +1 punkt procentowy szansy na unik. Unik anuluje całe trafienie.", "piloting": "Malejące korzyści: 37,5% × punkty / (punkty + 10). Maksymalnie 25% uniku przy 20 punktach. Unik anuluje całe trafienie.",
"gunner": "Każdy poziom: +1% obrażeń wszystkich broni.", "gunner": "Każdy punkt: +2% obrażeń wszystkich broni. Maksymalnie +40% przy 20 punktach.",
"mechanic": "Każdy poziom: otrzymywane obrażenia mniejsze o 1% (przed tarczami i kadłubem).", "mechanic": "Malejące korzyści: 50% × punkty / (punkty + 10). Maksymalnie 33,3% redukcji przy 20 punktach, przed tarczami i kadłubem.",
"sharp_eye": "Każdy poziom: +1 punkt procentowy szansy na trafienie krytyczne.", "sharp_eye": "Każdy punkt: +1 punkt procentowy szansy na trafienie krytyczne. Maksymalnie +20 przy 20 punktach.",
} }
static func skill_bonus(skill: String, rank: int) -> float:
var points := float(clampi(rank, 0, MAX_SKILL_RANK))
match skill:
"piloting":
return 0.375 * points / (points + 10.0)
"mechanic":
return 0.5 * points / (points + 10.0)
"gunner":
return 0.02 * points
"sharp_eye":
return 0.01 * points
return 0.0
static func xp_required(level: int) -> int: static func xp_required(level: int) -> int:
return XP_BASE + XP_PER_LEVEL * (maxi(1, level) - 1) return XP_BASE + XP_PER_LEVEL * (maxi(1, level) - 1)

View File

@ -18,6 +18,43 @@ func get_xp_required() -> int:
func get_skill_rank(skill: String) -> int: func get_skill_rank(skill: String) -> int:
return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK) return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK)
func get_skill_bonus(skill: String) -> float:
return BALANCE.skill_bonus(skill, get_skill_rank(skill))
func normalize_skill_ranks() -> void:
var changed_ranks := false
for skill in BALANCE.SKILL_NAMES:
var previous := maxi(0, int(skills.get(skill, 0)))
var rank := mini(previous, BALANCE.MAX_SKILL_RANK)
learning_points += previous - rank
if int(skills.get(skill, 0)) != rank:
changed_ranks = true
skills[skill] = rank
if changed_ranks:
emit_changed()
func get_spent_learning_points() -> int:
var spent := 0
for skill in BALANCE.SKILL_NAMES:
spent += maxi(0, int(skills.get(skill, 0)))
return spent
func get_skill_reset_cost() -> int:
return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level)
func can_reset_skills() -> bool:
return get_spent_learning_points() > 0 and credits >= get_skill_reset_cost()
func reset_skills() -> bool:
if not can_reset_skills():
return false
credits -= get_skill_reset_cost()
learning_points += get_spent_learning_points()
for skill in BALANCE.SKILL_NAMES:
skills[skill] = 0
emit_changed()
return true
func can_train(skill: String) -> bool: func can_train(skill: String) -> bool:
return skills.has(skill) and learning_points > 0 and get_skill_rank(skill) < BALANCE.MAX_SKILL_RANK return skills.has(skill) and learning_points > 0 and get_skill_rank(skill) < BALANCE.MAX_SKILL_RANK

View File

@ -8,6 +8,7 @@ var _thrust_strength := 0.0
var _engine_phase := 0.0 var _engine_phase := 0.0
var _trail_emission_timer := 0.0 var _trail_emission_timer := 0.0
var _trail_particles: Array[Dictionary] = [] var _trail_particles: Array[Dictionary] = []
var _mini_boost := false
func _ready() -> void: func _ready() -> void:
_ship = get_parent() as CharacterBody2D _ship = get_parent() as CharacterBody2D
@ -24,7 +25,8 @@ func _process(delta: float) -> void:
global_position = _ship.global_position global_position = _ship.global_position
global_rotation = _ship.global_rotation global_rotation = _ship.global_rotation
_engine_phase += delta _engine_phase += delta
var thrust_active := Input.is_action_pressed("ship_thrust") _mini_boost = bool(_ship.call("is_mini_boosting"))
var thrust_active := Input.is_action_pressed("ship_thrust") or _mini_boost
if bool(_ship.get("_is_destroyed")): if bool(_ship.get("_is_destroyed")):
thrust_active = false thrust_active = false
var target_strength := 1.0 if thrust_active else 0.0 var target_strength := 1.0 if thrust_active else 0.0
@ -52,6 +54,7 @@ func _spawn_trail_particle() -> void:
"age": 0.0, "age": 0.0,
"life": life, "life": life,
"size": randf_range(1.5, 3.0), "size": randf_range(1.5, 3.0),
"boost": _mini_boost,
}) })
func _update_trail_particles(delta: float) -> void: func _update_trail_particles(delta: float) -> void:
@ -69,7 +72,10 @@ func _draw() -> void:
return return
var pulse := 0.82 + 0.18 * sin(_engine_phase * 25.0) var pulse := 0.82 + 0.18 * sin(_engine_phase * 25.0)
var length := (28.0 + pulse * 14.0) * _thrust_strength var length := (28.0 + pulse * 14.0) * _thrust_strength * (1.35 if _mini_boost else 1.0)
var outer_color := Color(1.0, 0.28, 0.03) if _mini_boost else Color(0.04, 0.58, 1.0)
var inner_color := Color(1.0, 0.62, 0.12) if _mini_boost else Color(0.24, 0.9, 1.0)
var core_color := Color(1.0, 0.92, 0.7) if _mini_boost else Color(0.88, 0.98, 1.0)
var base_y := 22.0 var base_y := 22.0
var outer_tip := base_y + length var outer_tip := base_y + length
var inner_tip := base_y + length * 0.76 var inner_tip := base_y + length * 0.76
@ -78,15 +84,15 @@ func _draw() -> void:
draw_colored_polygon(PackedVector2Array([ draw_colored_polygon(PackedVector2Array([
Vector2(-5.2, base_y), Vector2(-3.2, base_y + 6.0), Vector2(0.0, outer_tip), Vector2(-5.2, base_y), Vector2(-3.2, base_y + 6.0), Vector2(0.0, outer_tip),
Vector2(3.2, base_y + 6.0), Vector2(5.2, base_y), Vector2(3.2, base_y + 6.0), Vector2(5.2, base_y),
]), Color(0.04, 0.58, 1.0, 0.48 * _thrust_strength)) ]), Color(outer_color, 0.48 * _thrust_strength))
draw_colored_polygon(PackedVector2Array([ draw_colored_polygon(PackedVector2Array([
Vector2(-2.8, base_y), Vector2(-1.8, base_y + 4.0), Vector2(0.0, inner_tip), Vector2(-2.8, base_y), Vector2(-1.8, base_y + 4.0), Vector2(0.0, inner_tip),
Vector2(1.8, base_y + 4.0), Vector2(2.8, base_y), Vector2(1.8, base_y + 4.0), Vector2(2.8, base_y),
]), Color(0.24, 0.9, 1.0, 0.78 * _thrust_strength)) ]), Color(inner_color, 0.78 * _thrust_strength))
draw_colored_polygon(PackedVector2Array([ draw_colored_polygon(PackedVector2Array([
Vector2(-1.0, base_y), Vector2(0.0, core_tip), Vector2(1.0, base_y), Vector2(-1.0, base_y), Vector2(0.0, core_tip), Vector2(1.0, base_y),
]), Color(0.88, 0.98, 1.0, 0.92 * _thrust_strength)) ]), Color(core_color, 0.92 * _thrust_strength))
draw_circle(Vector2(0.0, base_y), 2.3, Color(0.42, 0.88, 1.0, 0.82 * _thrust_strength)) draw_circle(Vector2(0.0, base_y), 2.3, Color(inner_color, 0.82 * _thrust_strength))
func _draw_trail() -> void: func _draw_trail() -> void:
for particle in _trail_particles: for particle in _trail_particles:
@ -98,5 +104,7 @@ func _draw_trail() -> void:
var particle_size := float(particle["size"]) var particle_size := float(particle["size"])
var current_local := to_local(current_position) var current_local := to_local(current_position)
var tail_local := to_local(current_position - particle_velocity.normalized() * particle_size * 3.5) var tail_local := to_local(current_position - particle_velocity.normalized() * particle_size * 3.5)
draw_line(tail_local, current_local, Color(0.22, 0.78, 1.0, fade * 0.52), particle_size * 0.72, true) var trail_color := Color(1.0, 0.42, 0.06) if bool(particle.get("boost", false)) else Color(0.22, 0.78, 1.0)
draw_circle(current_local, particle_size * fade, Color(0.62, 0.94, 1.0, fade * 0.72)) var particle_color := Color(1.0, 0.76, 0.3) if bool(particle.get("boost", false)) else Color(0.62, 0.94, 1.0)
draw_line(tail_local, current_local, Color(trail_color, fade * 0.52), particle_size * 0.72, true)
draw_circle(current_local, particle_size * fade, Color(particle_color, fade * 0.72))

View File

@ -72,6 +72,7 @@ func _process(_delta: float) -> void:
var to_planet := goal.global_position - player.global_position var to_planet := goal.global_position - player.global_position
debug_info.text = """STEROWANIE debug_info.text = """STEROWANIE
W: ciąg S: hamulec / wsteczny W: ciąg S: hamulec / wsteczny
Tab: mini boost (+100%% prędkości, 20 energii/s)
Q / E: strafe A / D: obrót Q / E: strafe A / D: obrót
LPM / PPM: broń 1 / broń 2 LPM / PPM: broń 1 / broń 2
1–6: aktywne rozszerzenia 1–6: aktywne rozszerzenia

View File

@ -7,6 +7,7 @@ const COMBAT_ROLLS := preload("res://scripts/game/combat_rolls.gd")
const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd") const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd")
const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd") const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd")
const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd") const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd")
const MINI_BOOST_ENERGY_PER_SECOND := 20.0
@export var max_speed := 76.0 @export var max_speed := 76.0
@export var reverse_max_speed := 210.0 @export var reverse_max_speed := 210.0
@ -51,6 +52,9 @@ var _boost_speed := 0.0
var _boost_target := 0.0 var _boost_target := 0.0
var _boost_acceleration := 0.0 var _boost_acceleration := 0.0
var _boost_velocity_acceleration := 0.0 var _boost_velocity_acceleration := 0.0
var _mini_boost_active := false
var _mini_boost_exhausted := false
var _mini_boost_coast_speed := 0.0
@onready var _shield_visual: Node2D = $ShieldVisual @onready var _shield_visual: Node2D = $ShieldVisual
func _enter_tree() -> void: func _enter_tree() -> void:
@ -84,6 +88,7 @@ func _ready() -> void:
for index in 6: for index in 6:
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key) _ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
_ensure_input_action(&"ship_thrust", KEY_W) _ensure_input_action(&"ship_thrust", KEY_W)
_ensure_input_action(&"ship_mini_boost", KEY_TAB)
_ensure_input_action(&"ship_brake", KEY_S) _ensure_input_action(&"ship_brake", KEY_S)
_ensure_input_action(&"ship_turn_left", KEY_A) _ensure_input_action(&"ship_turn_left", KEY_A)
_ensure_input_action(&"ship_turn_right", KEY_D) _ensure_input_action(&"ship_turn_right", KEY_D)
@ -144,16 +149,16 @@ func take_damage(amount: float) -> void:
_destroy_ship() _destroy_ship()
func get_evasion_chance() -> float: func get_evasion_chance() -> float:
return GameState.captain.get_skill_rank("piloting") / 100.0 return GameState.captain.get_skill_bonus("piloting")
func get_damage_reduction() -> float: func get_damage_reduction() -> float:
return GameState.captain.get_skill_rank("mechanic") / 100.0 return GameState.captain.get_skill_bonus("mechanic")
func get_weapon_damage_multiplier() -> float: func get_weapon_damage_multiplier() -> float:
return damage_multiplier * (1.0 + GameState.captain.get_skill_rank("gunner") / 100.0) return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner"))
func get_critical_chance() -> float: func get_critical_chance() -> float:
return clampf(critical_chance_bonus + GameState.captain.get_skill_rank("sharp_eye") / 100.0, 0.0, 1.0) return clampf(critical_chance_bonus + GameState.captain.get_skill_bonus("sharp_eye"), 0.0, 1.0)
func get_critical_multiplier() -> float: func get_critical_multiplier() -> float:
return CAPTAIN_BALANCE.BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, critical_damage_bonus)) return CAPTAIN_BALANCE.BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, critical_damage_bonus))
@ -201,10 +206,19 @@ func _restart_level_after_destruction() -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
if _is_destroyed or is_docked: if _is_destroyed or is_docked:
_mini_boost_active = false
return return
_contact_damage_timer = maxf(0.0, _contact_damage_timer - delta) _contact_damage_timer = maxf(0.0, _contact_damage_timer - delta)
energy = minf(max_energy, energy + (energy_regeneration + get_solar_regeneration()) * delta) energy = minf(max_energy, energy + (energy_regeneration + get_solar_regeneration()) * delta)
var was_boosting := is_boosting() var was_mini_boosting := _mini_boost_active
_update_mini_boost(delta)
if _mini_boost_active:
_mini_boost_coast_speed = 0.0
else:
if was_mini_boosting:
_mini_boost_coast_speed = maxf(0.0, velocity.length() - max_speed)
_mini_boost_coast_speed = move_toward(_mini_boost_coast_speed, 0.0, space_drag * delta)
var was_boosting := _boost_target > 0.0 or _boost_speed > 0.0
if _boost_target > 0.0: if _boost_target > 0.0:
_boost_speed = move_toward(_boost_speed, _boost_target, _boost_acceleration * delta) _boost_speed = move_toward(_boost_speed, _boost_target, _boost_acceleration * delta)
velocity = velocity.move_toward(Vector2.UP.rotated(rotation) * (max_speed + _boost_target), _boost_velocity_acceleration * delta) velocity = velocity.move_toward(Vector2.UP.rotated(rotation) * (max_speed + _boost_target), _boost_velocity_acceleration * delta)
@ -240,11 +254,12 @@ func _physics_process(delta: float) -> void:
var reverse_speed := velocity.dot(forward) var reverse_speed := velocity.dot(forward)
if reverse_speed < -reverse_max_speed: if reverse_speed < -reverse_max_speed:
velocity += forward * (-reverse_max_speed - reverse_speed) velocity += forward * (-reverse_max_speed - reverse_speed)
elif Input.is_action_pressed("ship_thrust"): elif Input.is_action_pressed("ship_thrust") or _mini_boost_active:
velocity += forward * thrust_acceleration * delta velocity += forward * thrust_acceleration * (2.0 if _mini_boost_active else 1.0) * delta
var forward_speed := velocity.dot(forward) var forward_speed := velocity.dot(forward)
if forward_speed > max_speed and not was_boosting: var forward_limit := _get_allowed_speed()
velocity += forward * (max_speed - forward_speed) if forward_speed > forward_limit and not was_boosting:
velocity += forward * (forward_limit - forward_speed)
var strafe_input := Input.get_axis("ship_strafe_left", "ship_strafe_right") var strafe_input := Input.get_axis("ship_strafe_left", "ship_strafe_right")
var right := Vector2.RIGHT.rotated(rotation) var right := Vector2.RIGHT.rotated(rotation)
@ -255,6 +270,7 @@ func _physics_process(delta: float) -> void:
if ( if (
not was_boosting not was_boosting
and not _mini_boost_active
and and
not Input.is_action_pressed("ship_brake") not Input.is_action_pressed("ship_brake")
and not Input.is_action_pressed("ship_thrust") and not Input.is_action_pressed("ship_thrust")
@ -269,13 +285,38 @@ func _physics_process(delta: float) -> void:
func _clamp_velocity_to_max_speed() -> void: func _clamp_velocity_to_max_speed() -> void:
var speed_squared := velocity.length_squared() var speed_squared := velocity.length_squared()
var allowed_speed := max_speed + _boost_speed var allowed_speed := _get_allowed_speed()
var max_speed_squared := allowed_speed * allowed_speed var max_speed_squared := allowed_speed * allowed_speed
if speed_squared > max_speed_squared: if speed_squared > max_speed_squared:
velocity = velocity.normalized() * allowed_speed velocity = velocity.normalized() * allowed_speed
func is_boosting() -> bool: func is_boosting() -> bool:
return _boost_target > 0.0 or _boost_speed > 0.0 return _boost_target > 0.0 or _boost_speed > 0.0 or _mini_boost_active
func is_mini_boosting() -> bool:
return _mini_boost_active and not _is_destroyed and not is_docked
func _get_allowed_speed() -> float:
return maxf(max_speed + _boost_speed, max_speed * 2.0 if _mini_boost_active else max_speed + _mini_boost_coast_speed)
func _update_mini_boost(delta: float) -> void:
_mini_boost_active = false
if not Input.is_action_pressed("ship_mini_boost"):
_mini_boost_exhausted = false
return
if _mini_boost_exhausted or Input.is_action_pressed("ship_brake"):
return
var cost := MINI_BOOST_ENERGY_PER_SECOND * delta
if energy < cost or energy <= 0.0:
_mini_boost_exhausted = true
return
energy = maxf(0.0, energy - cost)
_mini_boost_active = true
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.is_action("ship_mini_boost"):
# Tab controls the engine rather than moving focus between HUD buttons.
get_viewport().set_input_as_handled()
func get_solar_regeneration() -> float: func get_solar_regeneration() -> float:
if _is_destroyed or is_docked or not is_instance_valid(charging_star): if _is_destroyed or is_docked or not is_instance_valid(charging_star):

View File

@ -24,7 +24,7 @@ func _process(delta: float) -> void:
if stream == null or not is_instance_valid(_ship): if stream == null or not is_instance_valid(_ship):
return return
var thrust_active := Input.is_action_pressed("ship_thrust") var thrust_active := Input.is_action_pressed("ship_thrust") or bool(_ship.call("is_mini_boosting"))
if bool(_ship.get("_is_destroyed")): if bool(_ship.get("_is_destroyed")):
thrust_active = false thrust_active = false
var target_volume := THRUST_VOLUME_DB if thrust_active else SILENT_VOLUME_DB var target_volume := THRUST_VOLUME_DB if thrust_active else SILENT_VOLUME_DB

View File

@ -1,6 +1,8 @@
extends Resource extends Resource
class_name ItemInstance class_name ItemInstance
const ENERGY_REGENERATION_BONUS_MULTIPLIER := 0.5
@export var definition: ItemDefinition @export var definition: ItemDefinition
@export_range(1, 1000, 1) var item_level: int = 1 @export_range(1, 1000, 1) var item_level: int = 1
@export var additional_modifiers: Array[ItemStatModifier] = [] @export var additional_modifiers: Array[ItemStatModifier] = []
@ -22,7 +24,7 @@ func get_stat_total(stat_key: String) -> float:
for modifier in additional_modifiers: for modifier in additional_modifiers:
if modifier and modifier.stat_key == stat_key: if modifier and modifier.stat_key == stat_key:
total += modifier.value total += modifier.value
return total return total * ENERGY_REGENERATION_BONUS_MULTIPLIER if stat_key == "energy_regeneration" else total
func get_damage_range() -> Vector2: func get_damage_range() -> Vector2:
if definition == null or definition.category != ItemDefinition.Category.WEAPON: if definition == null or definition.category != ItemDefinition.Category.WEAPON:
@ -108,7 +110,7 @@ func get_tooltip_text() -> String:
primary_text = definition.active_ability.description primary_text = definition.active_ability.description
var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text] var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text]
if definition.energy_regeneration_bonus > 0.0: if definition.energy_regeneration_bonus > 0.0:
lines.append("Regeneracja generatora: +%.1f /s" % (energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus)) lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER))
if definition.category == ItemDefinition.Category.WEAPON: if definition.category == ItemDefinition.Category.WEAPON:
lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.")
if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:

View File

@ -8,4 +8,5 @@ class_name ItemStatModifier
func get_display_text() -> String: func get_display_text() -> String:
var label := affix.display_name if affix else ItemInstance.stat_name(stat_key) var label := affix.display_name if affix else ItemInstance.stat_name(stat_key)
var suffix := affix.unit if affix else ("%" if stat_key.ends_with("_percent") else "/s") var suffix := affix.unit if affix else ("%" if stat_key.ends_with("_percent") else "/s")
return "%s: %s%.2f %s" % [label, "+" if value >= 0.0 else "", value, suffix] var displayed_value := value * ItemInstance.ENERGY_REGENERATION_BONUS_MULTIPLIER if stat_key == "energy_regeneration" else value
return "%s: %s%.2f %s" % [label, "+" if displayed_value >= 0.0 else "", displayed_value, suffix]

View File

@ -17,6 +17,7 @@ var _buttons: Dictionary = {}
func _ready() -> void: func _ready() -> void:
_captain = GameState.captain _captain = GameState.captain
_captain.normalize_skill_ranks()
for skill in BALANCE.SKILL_NAMES: for skill in BALANCE.SKILL_NAMES:
var row := HBoxContainer.new() var row := HBoxContainer.new()
row.name = skill row.name = skill
@ -28,13 +29,13 @@ func _ready() -> void:
title.add_theme_font_size_override("font_size", 11) title.add_theme_font_size_override("font_size", 11)
row.add_child(title) row.add_child(title)
var rank := Label.new() var rank := Label.new()
rank.custom_minimum_size.x = 22 rank.custom_minimum_size.x = 40
rank.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT rank.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
rank.add_theme_font_size_override("font_size", 11) rank.add_theme_font_size_override("font_size", 11)
row.add_child(rank) row.add_child(rank)
_rank_labels[skill] = rank _rank_labels[skill] = rank
var effect := Label.new() var effect := Label.new()
effect.custom_minimum_size.x = 58 effect.custom_minimum_size.x = 74
effect.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT effect.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
effect.add_theme_font_size_override("font_size", 10) effect.add_theme_font_size_override("font_size", 10)
row.add_child(effect) row.add_child(effect)
@ -65,8 +66,11 @@ func _refresh() -> void:
points_label.text = "Punkty: %d" % _captain.learning_points points_label.text = "Punkty: %d" % _captain.learning_points
for skill in _buttons: for skill in _buttons:
var rank := _captain.get_skill_rank(skill) var rank := _captain.get_skill_rank(skill)
_rank_labels[skill].text = str(rank) _rank_labels[skill].text = "%d/%d" % [rank, BALANCE.MAX_SKILL_RANK]
var suffix: String = {"piloting": "unik", "gunner": "DMG", "mechanic": "redukcji", "sharp_eye": "kryt"}[skill] var suffix: String = {"piloting": "unik", "gunner": "DMG", "mechanic": "red.", "sharp_eye": "kryt"}[skill]
_effect_labels[skill].text = "%d%% %s" % [rank, suffix] var bonus := _captain.get_skill_bonus(skill)
_effect_labels[skill].text = "%.1f%% %s" % [bonus * 100.0, suffix]
var next_bonus := BALANCE.skill_bonus(skill, rank + 1)
_buttons[skill].tooltip_text = BALANCE.SKILL_HELP[skill] + ("\nOsiągnięto limit 20 punktów." if rank >= BALANCE.MAX_SKILL_RANK else "\nNastępny punkt: %.1f%% → %.1f%%" % [bonus * 100.0, next_bonus * 100.0])
_buttons[skill].disabled = not _captain.can_train(skill) _buttons[skill].disabled = not _captain.can_train(skill)
critical_label.text = "Krytyk: %.1f%% · ×%.2f\nCzęści: %.1f" % [float(player.call("get_critical_chance")) * 100.0, float(player.call("get_critical_multiplier")), _captain.parts] critical_label.text = "Krytyk: %.1f%% · ×%.2f\nCzęści: %.1f" % [float(player.call("get_critical_chance")) * 100.0, float(player.call("get_critical_multiplier")), _captain.parts]

View File

@ -4,6 +4,7 @@ signal next_level_requested
@onready var next_button: Button = $Center/Panel/Contents/NextButton @onready var next_button: Button = $Center/Panel/Contents/NextButton
@onready var buy_ammunition_button: Button = $Center/Panel/Contents/BuyAmmunitionButton @onready var buy_ammunition_button: Button = $Center/Panel/Contents/BuyAmmunitionButton
@onready var reset_skills_button: Button = $Center/Panel/Contents/ResetSkillsButton
@onready var services_label: Label = $Center/Panel/Contents/Services @onready var services_label: Label = $Center/Panel/Contents/Services
@onready var status_label: Label = $Center/Panel/Contents/Status @onready var status_label: Label = $Center/Panel/Contents/Status
@onready var panel: PanelContainer = $Center/Panel @onready var panel: PanelContainer = $Center/Panel
@ -15,6 +16,7 @@ var _open_tween: Tween
func _ready() -> void: func _ready() -> void:
next_button.pressed.connect(func() -> void: next_level_requested.emit()) next_button.pressed.connect(func() -> void: next_level_requested.emit())
buy_ammunition_button.pressed.connect(_buy_ammunition) buy_ammunition_button.pressed.connect(_buy_ammunition)
reset_skills_button.pressed.connect(_reset_skills)
_inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory
if _inventory: if _inventory:
_inventory.ammunition_changed.connect(_refresh_ammunition_service) _inventory.ammunition_changed.connect(_refresh_ammunition_service)
@ -59,6 +61,9 @@ func _position_at_planet() -> void:
position = Vector2(clampf(target.x, 8.0, maxf(8.0, screen_right - size.x)), clampf(target.y, 8.0, maxf(8.0, viewport_size.y - size.y - 80.0))) position = Vector2(clampf(target.x, 8.0, maxf(8.0, screen_right - size.x)), clampf(target.y, 8.0, maxf(8.0, viewport_size.y - size.y - 80.0)))
func _refresh_ammunition_service() -> void: func _refresh_ammunition_service() -> void:
reset_skills_button.disabled = not GameState.captain.can_reset_skills()
reset_skills_button.text = "RESET SKILLI — %d KREDYTÓW" % GameState.captain.get_skill_reset_cost()
reset_skills_button.tooltip_text = "Zwraca wszystkie %d wydanych punktów nauki. Koszt: 100 kredytów × poziom kapitana." % GameState.captain.get_spent_learning_points()
buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition() buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition()
if _inventory: if _inventory:
buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()] buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()]
@ -67,3 +72,7 @@ func _refresh_ammunition_service() -> void:
func _buy_ammunition() -> void: func _buy_ammunition() -> void:
if visible and _inventory: if visible and _inventory:
_inventory.buy_ammunition() _inventory.buy_ammunition()
func _reset_skills() -> void:
if visible:
GameState.captain.reset_skills()

View File

@ -0,0 +1,72 @@
extends Node2D
const PLAYER := preload("res://scenes/game/player_ship.tscn")
func _ready() -> void:
GameState.start_new_run()
var ship := PLAYER.instantiate() as CharacterBody2D
add_child(ship)
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
ship.set("energy_regeneration", 0.0)
ship.set("energy", 100.0)
var tab := InputEventKey.new()
tab.physical_keycode = KEY_TAB
assert(InputMap.action_has_event("ship_mini_boost", tab))
Input.action_press("ship_mini_boost")
ship.call("_physics_process", 1.0)
assert(ship.call("is_mini_boosting"))
assert(is_equal_approx(ship.get("energy"), 80.0))
assert(is_equal_approx(ship.velocity.length(), ship.get("max_speed") * 2.0))
var engine := ship.get_node("EngineVisual")
engine.call("_process", 0.2)
assert(engine.get("_mini_boost"))
var particles: Array = engine.get("_trail_particles")
assert(not particles.is_empty() and particles[0]["boost"])
Input.action_release("ship_mini_boost")
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting"))
var released_speed := ship.velocity.length()
assert(released_speed > ship.get("max_speed"))
assert(is_equal_approx(released_speed, ship.get("max_speed") * 2.0 - ship.get("space_drag") * 0.1))
Input.action_press("ship_thrust")
ship.call("_physics_process", 0.1)
assert(is_equal_approx(ship.velocity.length(), released_speed - ship.get("space_drag") * 0.1))
Input.action_release("ship_thrust")
for frame in 120:
ship.call("_physics_process", 0.1)
assert(ship.velocity.length() <= ship.get("max_speed"))
ship.set("energy", 0.0)
Input.action_press("ship_mini_boost")
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting"))
ship.set("energy", 100.0)
Input.action_release("ship_mini_boost")
ship.call("_physics_process", 0.1)
Input.action_press("ship_mini_boost")
ship.call("_physics_process", 1.0)
ship.set("energy", 0.0)
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting"))
assert(is_equal_approx(ship.velocity.length(), ship.get("max_speed") * 2.0 - ship.get("space_drag") * 0.1))
ship.set("energy", 20.0)
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting") and ship.get("energy") == 20.0)
Input.action_release("ship_mini_boost")
ship.call("_physics_process", 0.1)
Input.action_press("ship_mini_boost")
ship.call("_physics_process", 0.1)
assert(ship.call("is_mini_boosting") and ship.get("energy") == 18.0)
Input.action_press("ship_brake")
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting") and ship.get("energy") == 18.0)
Input.action_release("ship_brake")
ship.call("start_boost", 2.0, 0.5)
ship.set("_boost_speed", ship.get("max_speed") * 2.0)
ship.call("_physics_process", 0.1)
assert(ship.call("_get_allowed_speed") == ship.get("max_speed") * 3.0)
ship.set("is_docked", true)
ship.call("_physics_process", 0.1)
assert(not ship.call("is_mini_boosting"))
Input.action_release("ship_mini_boost")
print("MINI_BOOST_TEST: PASS (Tab mapping, double speed, energy drain, orange trail, release, exhaustion latch, braking, afterburner and docking)")

View File

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

View File

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

View File

@ -5,6 +5,7 @@ const COMBAT := preload("res://scripts/game/combat_rolls.gd")
const GAME_SCENE := preload("res://scenes/game/game.tscn") const GAME_SCENE := preload("res://scenes/game/game.tscn")
const GENERATOR := preload("res://scripts/items/item_generator.gd") const GENERATOR := preload("res://scripts/items/item_generator.gd")
const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn")
const BALANCE := preload("res://scripts/captain/captain_balance.gd")
class DamageReceiver: class DamageReceiver:
extends Node2D extends Node2D
@ -13,6 +14,7 @@ class DamageReceiver:
hits.append({"damage": amount, "critical": critical}) hits.append({"damage": amount, "critical": critical})
func _ready() -> void: func _ready() -> void:
_test_skill_balance_and_reset()
var profile := PROGRESS.new() var profile := PROGRESS.new()
assert(not profile.train_skill("gunner")) assert(not profile.train_skill("gunner"))
profile.add_rewards(99, 60) profile.add_rewards(99, 60)
@ -58,20 +60,21 @@ func _ready() -> void:
assert(panel.get_node("Contents/Header/Level").get("text") == "POZ. 2") assert(panel.get_node("Contents/Header/Level").get("text") == "POZ. 2")
var player := game.get_node("PlayerShip") as CharacterBody2D var player := game.get_node("PlayerShip") as CharacterBody2D
profile.skills["piloting"] = 100 profile.skills["piloting"] = 100
var before := Vector2(float(player.get("hull")), float(player.get("shields"))) assert(is_equal_approx(float(player.call("get_evasion_chance")), 0.25))
player.call("take_damage", 1000.0) assert(COMBAT.incoming_hit(100, player.call("get_evasion_chance"), 0, 0.0)["evaded"])
assert(before == Vector2(float(player.get("hull")), float(player.get("shields")))) assert(COMBAT.incoming_hit(100, player.call("get_evasion_chance"), 0, 0.25)["damage"] == 100)
profile.skills["piloting"] = 0 profile.skills["piloting"] = 0
profile.skills["mechanic"] = 50 profile.skills["mechanic"] = 15
player.set("shields", 100.0) # Fixed fixture, independent of selected chassis capacity. player.set("shields", 100.0) # Fixed fixture, independent of selected chassis capacity.
player.call("take_damage", 100.0) player.call("take_damage", 100.0)
assert(float(player.get("shields")) == 50 and float(player.get("hull")) == 100) assert(is_equal_approx(float(player.get("shields")), 30) and float(player.get("hull")) == 100)
player.set("shields", 0.0) player.set("shields", 0.0)
player.call("take_damage", 40.0) player.call("take_damage", 40.0)
assert(float(player.get("hull")) == 80) assert(is_equal_approx(float(player.get("hull")), 72))
profile.skills["mechanic"] = 100 profile.skills["mechanic"] = 100
player.call("take_damage", 1000000.0) assert(is_equal_approx(float(player.call("get_damage_reduction")), 1.0 / 3.0))
assert(float(player.get("hull")) == 80) player.call("take_damage", 30.0)
assert(is_equal_approx(float(player.get("hull")), 52))
profile.skills["mechanic"] = 0 profile.skills["mechanic"] = 0
profile.skills["sharp_eye"] = 1 profile.skills["sharp_eye"] = 1
var inventory := player.get_node("ShipInventory") as ShipInventory var inventory := player.get_node("ShipInventory") as ShipInventory
@ -88,20 +91,23 @@ func _ready() -> void:
assert(inventory.add_item(weapon) and inventory.move_item("cargo", free_slot, "weapon", 0)) assert(inventory.add_item(weapon) and inventory.move_item("cargo", free_slot, "weapon", 0))
assert(is_equal_approx(float(player.call("get_critical_chance")), 0.26)) assert(is_equal_approx(float(player.call("get_critical_chance")), 0.26))
assert(is_equal_approx(float(player.call("get_critical_multiplier")), 2.4)) assert(is_equal_approx(float(player.call("get_critical_multiplier")), 2.4))
assert(is_equal_approx(float(player.call("get_weapon_damage_multiplier")), 1.111)) assert(is_equal_approx(float(player.call("get_weapon_damage_multiplier")), 1.122))
profile.skills["sharp_eye"] = 100 profile.skills["sharp_eye"] = 20
assert(is_equal_approx(float(player.call("get_critical_chance")), 0.45))
player.set("critical_chance_bonus", 0.8) # Force this metadata check independently of RNG.
hit = player.call("roll_weapon_hit", weapon) hit = player.call("roll_weapon_hit", weapon)
assert(hit["critical"] and is_equal_approx(hit["damage"], 26.664)) assert(hit["critical"] and is_equal_approx(hit["damage"], 26.928))
player.set("critical_chance_bonus", 0.25)
var receiver := DamageReceiver.new() var receiver := DamageReceiver.new()
game.add_child(receiver) game.add_child(receiver)
var projectile := PROJECTILE.instantiate() var projectile := PROJECTILE.instantiate()
projectile.call("configure", Vector2.ZERO, Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 400.0, 26.664, 1000.0, null, true) projectile.call("configure", Vector2.ZERO, Vector2.RIGHT, ItemDefinition.WeaponKind.MISSILE, 400.0, 26.928, 1000.0, null, true)
game.add_child(projectile) game.add_child(projectile)
projectile.set_physics_process(false) projectile.set_physics_process(false)
projectile.call("_apply_damage_once", receiver) projectile.call("_apply_damage_once", receiver)
projectile.call("_apply_damage_once", receiver) projectile.call("_apply_damage_once", receiver)
assert(receiver.hits.size() == 1 and receiver.hits[0]["critical"]) assert(receiver.hits.size() == 1 and receiver.hits[0]["critical"])
assert(is_equal_approx(receiver.hits[0]["damage"], 26.664)) assert(is_equal_approx(receiver.hits[0]["damage"], 26.928))
player.call("take_damage", 1000000.0) player.call("take_damage", 1000000.0)
await get_tree().create_timer(1.3).timeout await get_tree().create_timer(1.3).timeout
game = get_tree().current_scene as Node2D game = get_tree().current_scene as Node2D
@ -125,12 +131,50 @@ func _ready() -> void:
game = get_tree().current_scene as Node2D game = get_tree().current_scene as Node2D
_freeze(game) _freeze(game)
assert(GameState.captain == profile and GameState.system_level == 2) assert(GameState.captain == profile and GameState.system_level == 2)
assert(profile.get_skill_rank("gunner") == 1 and profile.get_skill_rank("sharp_eye") == 100) assert(profile.get_skill_rank("gunner") == 1 and profile.get_skill_rank("sharp_eye") == 20)
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("take_damage", 1000000.0) enemy.call("take_damage", 1000000.0)
assert(profile.level == 5 and profile.experience == 15 and profile.credits == 641) assert(profile.level == 5 and profile.experience == 15 and profile.credits == 641)
print("CAPTAIN_SYSTEM_TEST: PASS (XP, multiple levels, learning UI, rewards once, dodge, reduction, critical addons, projectile metadata, death and next system)") print("CAPTAIN_SYSTEM_TEST: PASS (XP, multiple levels, learning UI, rewards once, dodge, reduction, critical addons, projectile metadata, death and next system)")
func _test_skill_balance_and_reset() -> void:
assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 1), 0.5 / 11.0))
assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 5), 1.0 / 6.0))
assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 10), 0.25))
assert(is_equal_approx(BALANCE.skill_bonus("mechanic", 20), 1.0 / 3.0))
assert(is_equal_approx(BALANCE.skill_bonus("piloting", 20), 0.25))
assert(is_equal_approx(BALANCE.skill_bonus("gunner", 20), 0.4))
assert(is_equal_approx(BALANCE.skill_bonus("sharp_eye", 20), 0.2))
for skill in ["mechanic", "piloting"]:
var previous_gain := 1.0
for rank in range(1, 21):
var gain := BALANCE.skill_bonus(skill, rank) - BALANCE.skill_bonus(skill, rank - 1)
assert(gain > 0.0 and gain < previous_gain)
previous_gain = gain
var captain := PROGRESS.new()
captain.learning_points = 100
for rank in 20:
assert(captain.train_skill("mechanic"))
assert(not captain.train_skill("mechanic") and captain.learning_points == 80)
captain.level = 3
captain.credits = 299
assert(not captain.reset_skills())
assert(captain.skills["mechanic"] == 20 and captain.credits == 299)
captain.credits = 300
assert(captain.reset_skills())
assert(captain.learning_points == 100 and captain.credits == 0 and captain.skills["mechanic"] == 0)
captain.credits = 300
assert(not captain.reset_skills() and captain.credits == 300)
for skill in BALANCE.SKILL_NAMES:
captain.skills[skill] = 100
captain.learning_points = 7
captain.normalize_skill_ranks()
assert(captain.learning_points == 327 and captain.get_spent_learning_points() == 80)
captain.normalize_skill_ranks()
assert(captain.learning_points == 327)
assert(captain.reset_skills())
assert(captain.learning_points == 407)
func _freeze(game: Node2D) -> void: func _freeze(game: Node2D) -> void:
game.set_physics_process(false) game.set_physics_process(false)
game.get_node("PlayerShip").set_physics_process(false) game.get_node("PlayerShip").set_physics_process(false)

View File

@ -60,6 +60,15 @@ func _ready() -> void:
GameState.captain.credits = inventory.get_ammunition_buy_cost() GameState.captain.credits = inventory.get_ammunition_buy_cost()
menu.get_node("Center/Panel/Contents/BuyAmmunitionButton").emit_signal("pressed") menu.get_node("Center/Panel/Contents/BuyAmmunitionButton").emit_signal("pressed")
assert(inventory.ammunition == 400 and GameState.captain.credits == 0) assert(inventory.ammunition == 400 and GameState.captain.credits == 0)
assert(GameState.captain.train_skill("gunner"))
GameState.captain.credits = GameState.captain.get_skill_reset_cost()
GameState.captain.emit_changed()
var reset_button: Button = menu.get_node("Center/Panel/Contents/ResetSkillsButton")
assert(not reset_button.disabled)
reset_button.emit_signal("pressed")
assert(GameState.captain.get_skill_rank("gunner") == 0)
assert(GameState.captain.learning_points == 1 and GameState.captain.credits == 0)
assert(reset_button.disabled)
player.position = initial_data["player_start"] player.position = initial_data["player_start"]
game.call("_physics_process", 0.0) game.call("_physics_process", 0.0)
assert(not menu.visible) assert(not menu.visible)