From f12ed2f67ebd51b31db3170c1eb25286a3d20678 Mon Sep 17 00:00:00 2001 From: Tomasz Boruc Date: Tue, 13 Oct 2026 20:10:16 +0200 Subject: [PATCH] Zmiany w brocniah i dodanie ebsorcji --- assets/icons/gamma.svg | 1 + assets/icons/gamma.svg.import | 43 ++++++++++ resources/items/affix_database.tres | 8 +- .../items/affixes/energy_absorption.tres | 13 +++ .../items/affixes/kinetic_absorption.tres | 13 +++ .../items/affixes/radiation_absorption.tres | 13 +++ resources/items/gamma_cannon.tres | 21 +++++ scripts/captain/captain_progress.gd | 2 +- scripts/game/damage_types.gd | 28 +++++++ scripts/game/damage_types.gd.uid | 1 + scripts/game/enemy_health_bar.gd | 2 + scripts/game/enemy_plasma_projectile.gd | 5 +- scripts/game/enemy_ship.gd | 84 +++++++++++++++---- scripts/game/flight_hud.gd | 6 +- scripts/game/radiation_status.gd | 43 ++++++++++ scripts/game/radiation_status.gd.uid | 1 + scripts/game/ship_controller.gd | 6 +- scripts/game/weapon_audio.gd | 2 + scripts/game/weapon_controller.gd | 4 +- scripts/game/weapon_projectile.gd | 29 ++++++- scripts/items/item_definition.gd | 5 +- scripts/items/item_generator.gd | 1 + scripts/items/item_instance.gd | 7 +- scripts/items/ship_inventory.gd | 55 +++++++++++- scripts/levels/exploration_poi.gd | 8 +- scripts/ui/enemy_drawer.gd | 60 +++++++++++++ scripts/ui/enemy_drawer.gd.uid | 1 + scripts/ui/flight_cockpit.gd | 2 + scripts/ui/planet_services_menu.gd | 12 +++ scripts/ui/ship_crafting_panel.gd | 5 ++ scripts/ui/ship_inventory_panel.gd | 1 + tests/captain/captain_system_test.gd | 2 +- tests/items/affix_database_test.gd | 11 ++- tests/items/credit_pickup_test.gd | 2 +- tests/items/damage_types_test.gd | 84 +++++++++++++++++++ tests/items/damage_types_test.gd.uid | 1 + tests/items/damage_types_test.tscn | 4 + .../items/machine_gun_shield_recovery_test.gd | 1 + tests/items/shield_on_damage_test.gd | 1 + tests/items/weapon_damage_test.gd | 2 +- tests/levels/system_lifecycle_test.gd | 3 + 41 files changed, 548 insertions(+), 45 deletions(-) create mode 100644 assets/icons/gamma.svg create mode 100644 assets/icons/gamma.svg.import create mode 100644 resources/items/affixes/energy_absorption.tres create mode 100644 resources/items/affixes/kinetic_absorption.tres create mode 100644 resources/items/affixes/radiation_absorption.tres create mode 100644 resources/items/gamma_cannon.tres create mode 100644 scripts/game/damage_types.gd create mode 100644 scripts/game/damage_types.gd.uid create mode 100644 scripts/game/radiation_status.gd create mode 100644 scripts/game/radiation_status.gd.uid create mode 100644 scripts/ui/enemy_drawer.gd create mode 100644 scripts/ui/enemy_drawer.gd.uid create mode 100644 tests/items/damage_types_test.gd create mode 100644 tests/items/damage_types_test.gd.uid create mode 100644 tests/items/damage_types_test.tscn diff --git a/assets/icons/gamma.svg b/assets/icons/gamma.svg new file mode 100644 index 0000000..a0dcf4d --- /dev/null +++ b/assets/icons/gamma.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/gamma.svg.import b/assets/icons/gamma.svg.import new file mode 100644 index 0000000..585780a --- /dev/null +++ b/assets/icons/gamma.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b30nivtfvxw31" +path="res://.godot/imported/gamma.svg-877dc4511099b181b72b5bbafe97b7fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/gamma.svg" +dest_files=["res://.godot/imported/gamma.svg-877dc4511099b181b72b5bbafe97b7fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/resources/items/affix_database.tres b/resources/items/affix_database.tres index a4dbd04..64120b0 100644 --- a/resources/items/affix_database.tres +++ b/resources/items/affix_database.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=15 format=3] +[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=18 format=3] [ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"] [ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"] @@ -17,6 +17,10 @@ [ext_resource type="Resource" path="res://resources/items/affixes/shield_on_damage.tres" id="14_affix"] +[ext_resource type="Resource" path="res://resources/items/affixes/kinetic_absorption.tres" id="kinetic"] +[ext_resource type="Resource" path="res://resources/items/affixes/energy_absorption.tres" id="energy"] +[ext_resource type="Resource" path="res://resources/items/affixes/radiation_absorption.tres" id="radiation"] + [resource] script = ExtResource("1_database") -affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix")]) +affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix"), ExtResource("kinetic"), ExtResource("energy"), ExtResource("radiation")]) diff --git a/resources/items/affixes/energy_absorption.tres b/resources/items/affixes/energy_absorption.tres new file mode 100644 index 0000000..7fc3ec3 --- /dev/null +++ b/resources/items/affixes/energy_absorption.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1"] +[resource] +script = ExtResource("1") +affix_id = "energy_absorption" +display_name = "Absorpcja obrażeń energetycznych" +stat_key = "energy_absorption" +description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny limit statku: 60%." +category_mask = 95 +minimum_value_level_1 = 1.0 +maximum_value_level_1 = 1.0 +minimum_value_level_1000 = 10.0 +maximum_value_level_1000 = 15.0 diff --git a/resources/items/affixes/kinetic_absorption.tres b/resources/items/affixes/kinetic_absorption.tres new file mode 100644 index 0000000..e588785 --- /dev/null +++ b/resources/items/affixes/kinetic_absorption.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1"] +[resource] +script = ExtResource("1") +affix_id = "kinetic_absorption" +display_name = "Absorpcja obrażeń kinetycznych" +stat_key = "kinetic_absorption" +description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny limit statku: 60%." +category_mask = 95 +minimum_value_level_1 = 1.0 +maximum_value_level_1 = 1.0 +minimum_value_level_1000 = 10.0 +maximum_value_level_1000 = 15.0 diff --git a/resources/items/affixes/radiation_absorption.tres b/resources/items/affixes/radiation_absorption.tres new file mode 100644 index 0000000..392fc3e --- /dev/null +++ b/resources/items/affixes/radiation_absorption.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3] +[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1"] +[resource] +script = ExtResource("1") +affix_id = "radiation_absorption" +display_name = "Absorpcja obrażeń radiacyjnych" +stat_key = "radiation_absorption" +description = "Niweluje obrażenia tego typu przed tarczą i pancerzem. Łączny limit statku: 60%." +category_mask = 95 +minimum_value_level_1 = 1.0 +maximum_value_level_1 = 1.0 +minimum_value_level_1000 = 10.0 +maximum_value_level_1000 = 15.0 diff --git a/resources/items/gamma_cannon.tres b/resources/items/gamma_cannon.tres new file mode 100644 index 0000000..0fa1c74 --- /dev/null +++ b/resources/items/gamma_cannon.tres @@ -0,0 +1,21 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"] +[ext_resource type="Texture2D" path="res://assets/icons/gamma.svg" id="icon"] +[resource] +script = ExtResource("1") +icon = ExtResource("icon") +item_id = "gamma_cannon" +display_name = "Działko gamma" +category = 0 +description = "Radiacyjna kula. Napromieniowanie: 3 s, 25% obrażeń trafienia/s. Kolejne trafienia odświeżają czas. Zużywa napromieniowaną amunicję." +primary_stat = "damage" +primary_value = 15.0 +stat_suffix = " DMG" +weapon_kind = 4 +fire_interval = 0.8 +projectile_speed = 430.0 +weapon_range = 800.0 +damage_lower_roll_min = 10 +damage_lower_roll_max = 14 +damage_spread_roll_min = 4 +damage_spread_roll_max = 8 diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 3d7e1e3..e7b290c 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -7,7 +7,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd") @export var experience: int = 0 @export var credits: int = 0 @export var parts: float = 0.0 -@export var minerals: Dictionary = {"titanium": 0, "iron": 0, "aluminium": 0} +@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0} @export var learning_points: int = 0 @export var skills: Dictionary = { "piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0, diff --git a/scripts/game/damage_types.gd b/scripts/game/damage_types.gd new file mode 100644 index 0000000..af4cb29 --- /dev/null +++ b/scripts/game/damage_types.gd @@ -0,0 +1,28 @@ +extends RefCounted +class_name DamageTypes + +enum Type { KINETIC, ENERGY, RADIATION } +const KEYS := ["kinetic_absorption", "energy_absorption", "radiation_absorption"] +const NAMES := ["Kinetyczne", "Energetyczne", "Radiacyjne"] + +static func for_weapon(kind: int) -> int: + if kind == ItemDefinition.WeaponKind.GAMMA: + return Type.RADIATION + if kind in [ItemDefinition.WeaponKind.LASER, ItemDefinition.WeaponKind.PLASMA]: + return Type.ENERGY + return Type.KINETIC + +static func capped(values: Vector3) -> Vector3: + values = values.max(Vector3.ZERO) + var total := values.x + values.y + values.z + return values * (60.0 / total) if total > 60.0 else values + +static func enemy_profile(level: int, seed_value: int) -> Dictionary: + var rng := RandomNumberGenerator.new() + rng.seed = seed_value + var weights := Vector3(rng.randf_range(0.1, 1.0), rng.randf_range(0.1, 1.0), rng.randf_range(0.1, 1.0)) + var total := lerpf(2.0, 60.0, pow(float(clampi(level, 1, 1000) - 1) / 999.0, 1.25)) + return {"kind": rng.randi_range(0, 4), "absorption": weights * total / (weights.x + weights.y + weights.z)} + +static func describe(values: Vector3) -> String: + return "Absorpcja: K %.1f%% · E %.1f%% · R %.1f%%" % [values.x, values.y, values.z] diff --git a/scripts/game/damage_types.gd.uid b/scripts/game/damage_types.gd.uid new file mode 100644 index 0000000..f3432b8 --- /dev/null +++ b/scripts/game/damage_types.gd.uid @@ -0,0 +1 @@ +uid://cumj03bmuxvlr diff --git a/scripts/game/enemy_health_bar.gd b/scripts/game/enemy_health_bar.gd index 86dff48..adf58af 100644 --- a/scripts/game/enemy_health_bar.gd +++ b/scripts/game/enemy_health_bar.gd @@ -46,5 +46,7 @@ func _draw() -> void: var background := Rect2(-width * 0.5, -3.0, width, 6.0) draw_rect(background.grow(1.0), Color("08111dee")) draw_rect(background, Color("39232bee")) + draw_rect(Rect2(-width * 0.5, 5.0, width, 3.0), Color("142a3d")) + draw_rect(Rect2(-width * 0.5, 5.0, width * clampf(enemy.shields / maxf(1.0, enemy.max_shields), 0.0, 1.0), 3.0), Color("67cfff")) var color := _tier_color() draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color) diff --git a/scripts/game/enemy_plasma_projectile.gd b/scripts/game/enemy_plasma_projectile.gd index 6abb4ce..63ac22a 100644 --- a/scripts/game/enemy_plasma_projectile.gd +++ b/scripts/game/enemy_plasma_projectile.gd @@ -49,5 +49,8 @@ func _hit_target(target: Variant) -> void: get_parent().add_child(effect) effect.call("configure", false, Color(1.0, 0.32, 0.38)) if target is Node and target.has_method("take_damage"): - target.call("take_damage", damage) + if target is CharacterBody2D: + target.call("take_damage", damage, false, DamageTypes.Type.ENERGY) + else: + target.call("take_damage", damage) queue_free() diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 324f5c4..dcee445 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -2,7 +2,6 @@ extends CharacterBody2D const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn") const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn") -const PLASMA_PROJECTILE_SCENE := preload("res://scenes/game/enemy_plasma_projectile.tscn") const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd") const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd") const LOOT_PICKUP := preload("res://scripts/items/loot_pickup.gd") @@ -20,6 +19,12 @@ const LEVEL_BALANCE := preload("res://scripts/levels/level_balance.gd") @export var weapon_cooldown := 1.5 @export var weapon_damage := 8.0 +var max_shields := 20.0 +var shields := 20.0 +var absorption := Vector3.ZERO +var weapon_kind := ItemDefinition.WeaponKind.PLASMA +var _shield_visual: Node2D + var hull := 50.0 var _player: CharacterBody2D var _activated := false @@ -29,6 +34,11 @@ var _weapon_timer := 0.0 func _ready() -> void: hull = max_hull _sprite = $Sprite2D + _shield_visual = Node2D.new() + _shield_visual.set_script(preload("res://scripts/game/shield_visual.gd")) + _shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0 + add_child(_shield_visual) + _shield_visual.set_shield_values(shields, max_shields) add_to_group("enemies") _weapon_timer = randf_range(0.2, weapon_cooldown) @@ -42,6 +52,11 @@ func apply_level_settings(level: int, tier: int) -> void: var damage_bonus := 4.0 if tier == 2 else (1.3 if tier == 1 else 1.0) max_hull = 50.0 * power * hull_multiplier * combat_multiplier hull = max_hull + max_shields = max_hull * 0.4 + shields = max_shields + var profile := DamageTypes.enemy_profile(level, GameState.system_seed) + weapon_kind = int(profile["kind"]) + absorption = profile["absorption"] 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_acceleration = 85.0 * boss_multiplier @@ -50,6 +65,9 @@ func apply_level_settings(level: int, tier: int) -> void: weapon_range = 680.0 * boss_multiplier weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, 1000) - 1))) / boss_multiplier $CollisionShape2D.scale = Vector2.ONE * boss_multiplier + if is_instance_valid(_shield_visual): + _shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0 + _shield_visual.set_shield_values(shields, max_shields) func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void: _player = player @@ -98,31 +116,63 @@ func _check_player_collision() -> void: _player.call("register_enemy_collision", global_position) func _fire_plasma(direction: Vector2) -> void: - var projectile := PLASMA_PROJECTILE_SCENE.instantiate() as Area2D - projectile.call("configure", global_position + direction * (get_visual_size() * 0.5), direction, weapon_damage) - get_parent().add_child(projectile) - # Relative to the Plasma bus: enemy shots are 4 dB quieter than player shots. - WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, ItemDefinition.WeaponKind.PLASMA, -4.0) + var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0) + if weapon_kind == ItemDefinition.WeaponKind.LASER: + var endpoint := muzzle + direction * weapon_range + var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 3, [get_rid()]) + var hit := get_world_2d().direct_space_state.intersect_ray(query) + if not hit.is_empty(): + endpoint = hit["position"] + var target: Node = hit.get("collider") + if target is CharacterBody2D and target.has_method("take_damage"): + target.take_damage(weapon_damage, false, DamageTypes.Type.ENERGY) + var beam := Line2D.new() + beam.set_script(preload("res://scripts/game/laser_beam_visual.gd")) + beam.width = 4.0 + beam.default_color = Color(1.0, 0.3, 0.3) + beam.add_point(muzzle) + beam.add_point(endpoint) + get_parent().add_child(beam) + get_tree().create_timer(0.1).timeout.connect(beam.queue_free) + else: + var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + projectile.enemy_shot = true + projectile.collision_mask = 3 + projectile.missile_homing = false + var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0) + projectile.configure(muzzle, direction, weapon_kind, speed, weapon_damage, weapon_range) + get_parent().add_child(projectile) + WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0) -func take_weapon_hit(amount: float, critical: bool) -> void: - var actual_damage := minf(maxf(0.0, amount), maxf(0.0, hull)) - if is_queued_for_deletion(): - actual_damage = 0.0 - take_damage(amount, critical) - if is_instance_valid(_player): - _player.call("restore_shields_from_damage", actual_damage) - -func take_damage(amount: float, critical: bool = false) -> void: +func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> void: if is_queued_for_deletion() or hull <= 0.0: return - hull = maxf(0.0, hull - maxf(0.0, amount)) - _show_damage_popup(amount, critical) + var before := hull + shields + take_damage(amount, critical, damage_type) + if is_instance_valid(_player): + _player.call("restore_shields_from_damage", maxf(0.0, before - hull - shields)) + +func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC) -> void: + if is_queued_for_deletion() or hull <= 0.0: + return + var actual := maxf(0.0, amount) * (1.0 - absorption[damage_type] / 100.0) + var shield_damage := minf(shields, actual) + shields -= shield_damage + hull = maxf(0.0, hull - actual + shield_damage) + if is_instance_valid(_shield_visual): + _shield_visual.set_shield_values(shields, max_shields) + if shield_damage > 0.0: + _shield_visual.flash_hit() + _show_damage_popup(actual, critical) if hull <= 0.0: _reward_captain() _drop_loot() _spawn_destruction_effect() queue_free() +func get_weapon_dps() -> float: + return weapon_damage / maxf(0.001, weapon_cooldown) * (1.0 + 0.25 * minf(3.0, weapon_cooldown) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0) + func _reward_captain() -> void: if is_instance_valid(_player): var reward := GameState.captain.reward_enemy(GameState.system_level, loot_tier, float(_player.get("experience_multiplier")), float(_player.get("credits_multiplier")), false) diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 1babdb2..0c673c2 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -40,6 +40,10 @@ func _ready() -> void: # The display font has unusually tall vertical metrics; keep debug rows compact. debug_info.add_theme_constant_override("line_spacing", mini(0, 13 - ceili(debug_info.get_theme_font("font").get_height(10)))) _create_sidebar() + var enemy_panel := Control.new() + enemy_panel.name = "EnemyDrawer" + enemy_panel.set_script(preload("res://scripts/ui/enemy_drawer.gd")) + $HUDRoot.add_child(enemy_panel) _inventory_clip = Control.new() _inventory_clip.name = "InventoryDrawer" _inventory_clip.clip_contents = true @@ -154,7 +158,7 @@ func _layout_sidebar() -> void: _sidebar.scale = Vector2.ONE * EQUIPMENT_UI_SCALE _sidebar.size = Vector2(maxf(SIDEBAR_WIDTH, _sidebar.get_combined_minimum_size().x), minf(_sidebar.get_node("Scroll/Contents").get_combined_minimum_size().y + 16.0, maxf(100.0, ($HUDRoot.size.y - 64.0) / EQUIPMENT_UI_SCALE))) _cockpit.scale = Vector2.ONE * 0.85 - _cockpit.size = Vector2(minf(640.0, ($HUDRoot.size.x - 24) / 0.85), 126) + _cockpit.size = Vector2(minf(640.0, ($HUDRoot.size.x - 24) / 0.85), 170) _cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12) _layout_debug() _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 44) diff --git a/scripts/game/radiation_status.gd b/scripts/game/radiation_status.gd new file mode 100644 index 0000000..0ad2e63 --- /dev/null +++ b/scripts/game/radiation_status.gd @@ -0,0 +1,43 @@ +extends Node + +var remaining := 0.0 +var tick_damage := 0.0 +var credited_to_player := false +var _tick_elapsed := 0.0 +var _base_color := Color.WHITE +var _sprite: Sprite2D + +static func apply(target: Node, damage_per_second: float, player_hit: bool) -> void: + if not target.has_method("take_damage") or not target is CharacterBody2D: + return + var status := target.get_node_or_null("RadiationStatus") + if status == null: + status = Node.new() + status.set_script(load("res://scripts/game/radiation_status.gd")) + status.name = "RadiationStatus" + target.add_child(status) + status.remaining = 3.0 + status.tick_damage = maxf(status.tick_damage, damage_per_second) + status.credited_to_player = player_hit + +func _ready() -> void: + _sprite = get_parent().get_node_or_null("Sprite2D") + if _sprite != null: + _base_color = _sprite.modulate + +func _physics_process(delta: float) -> void: + var step := minf(delta, remaining) + remaining = maxf(0.0, remaining - delta) + var target := get_parent() + if _sprite != null: + _sprite.modulate = Color(0.3, 1.0, 0.35) if remaining > 0.0 else _base_color + _tick_elapsed += step + if _tick_elapsed >= 0.5 or (remaining <= 0.0 and _tick_elapsed > 0.0): + step = _tick_elapsed + _tick_elapsed = 0.0 + if credited_to_player and target.has_method("take_weapon_hit"): + target.take_weapon_hit(tick_damage * step, false, DamageTypes.Type.RADIATION) + else: + target.take_damage(tick_damage * step, false, DamageTypes.Type.RADIATION) + if remaining <= 0.0: + tick_damage = 0.0 diff --git a/scripts/game/radiation_status.gd.uid b/scripts/game/radiation_status.gd.uid new file mode 100644 index 0000000..059ff9c --- /dev/null +++ b/scripts/game/radiation_status.gd.uid @@ -0,0 +1 @@ +uid://drtwspjwle1py diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index b326f86..7f92b3e 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -35,6 +35,7 @@ var shields := 100.0 var energy := 100.0 var energy_regeneration := 5.0 var shield_regeneration := 0.0 +var absorption := Vector3.ZERO var damage_multiplier := 1.0 var experience_multiplier := 1.0 var credits_multiplier := 1.0 @@ -69,6 +70,7 @@ func _enter_tree() -> void: base_max_speed = float(chassis["speed"]) func apply_equipment_bonuses(bonuses: Dictionary) -> void: + absorption = DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0)))) max_hull = (base_max_hull + float(bonuses.get("max_hull", 0.0))) * (1.0 + float(bonuses.get("hull_percent", 0.0)) / 100.0) max_shields = (base_max_shields + float(bonuses.get("max_shields", 0.0))) * (1.0 + float(bonuses.get("shield_percent", 0.0)) / 100.0) scanner_range = base_scanner_range + float(bonuses.get("scanner_range", 0.0)) @@ -134,10 +136,10 @@ func _ensure_input_action(action_name: StringName, physical_key: Key) -> void: if not InputMap.action_has_event(action_name, event): InputMap.action_add_event(action_name, event) -func take_damage(amount: float) -> void: +func take_damage(amount: float, _critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC) -> void: if _is_destroyed or is_docked or amount <= 0.0: return - var hit := COMBAT_ROLLS.incoming_hit(amount, get_evasion_chance(), get_damage_reduction()) + var hit := COMBAT_ROLLS.incoming_hit(amount * (1.0 - absorption[damage_type] / 100.0), get_evasion_chance(), get_damage_reduction()) if hit["evaded"]: var flash := Node2D.new() flash.set_script(COMBAT_FLASH) diff --git a/scripts/game/weapon_audio.gd b/scripts/game/weapon_audio.gd index f951046..e24326f 100644 --- a/scripts/game/weapon_audio.gd +++ b/scripts/game/weapon_audio.gd @@ -2,6 +2,7 @@ extends RefCounted class_name WeaponAudio const SOUND_PATHS := { + ItemDefinition.WeaponKind.GAMMA: "res://assets/sounds/effects/plasma.mp3", ItemDefinition.WeaponKind.PLASMA: "res://assets/sounds/effects/plasma.mp3", ItemDefinition.WeaponKind.MACHINE_GUN: "res://assets/sounds/effects/machine_gun.mp3", ItemDefinition.WeaponKind.LASER: "res://assets/sounds/effects/laser.mp3", @@ -11,6 +12,7 @@ const SOUND_PATHS := { static var _streams: Dictionary = {} const BUS_NAMES := { + ItemDefinition.WeaponKind.GAMMA: &"Plasma", ItemDefinition.WeaponKind.PLASMA: &"Plasma", ItemDefinition.WeaponKind.MACHINE_GUN: &"MachineGun", ItemDefinition.WeaponKind.LASER: &"Laser", diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index 128729d..742347e 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -205,6 +205,8 @@ func _muzzle_position() -> Vector2: func _consume_shot_resource(item: ItemInstance) -> bool: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: return inventory.consume_ammunition() + if item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: + return inventory.consume_irradiated_ammunition() return _consume_energy(item.get_energy_cost()) func _consume_energy(amount: float) -> bool: @@ -216,7 +218,7 @@ func _consume_energy(amount: float) -> bool: func _apply_damage(target: Variant, damage: float, critical: bool = false) -> void: if target is Node and target.has_method("take_weapon_hit"): - target.call("take_weapon_hit", damage, critical) + target.call("take_weapon_hit", damage, critical, DamageTypes.Type.ENERGY) elif target is Node and target.has_method("take_damage"): target.call("take_damage", damage) diff --git a/scripts/game/weapon_projectile.gd b/scripts/game/weapon_projectile.gd index 2f34929..a39b572 100644 --- a/scripts/game/weapon_projectile.gd +++ b/scripts/game/weapon_projectile.gd @@ -4,6 +4,7 @@ const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn") const PLASMA_TEXTURE := preload("res://assets/other/bullet.png") const PLASMA_VISUAL_WIDTH := 24.0 +var enemy_shot := false var direction := Vector2.UP var projectile_speed := 600.0 var damage := 1.0 @@ -124,6 +125,9 @@ func _physics_process(delta: float) -> void: queue_free() func _process(delta: float) -> void: + if weapon_kind == ItemDefinition.WeaponKind.GAMMA: + _engine_time += delta + queue_redraw() if weapon_kind == ItemDefinition.WeaponKind.MISSILE: _engine_time += delta _smoke_emission_timer += delta @@ -157,6 +161,8 @@ func _update_smoke_puffs(delta: float) -> void: func _find_enemy_in_acquisition_range() -> Node2D: var nearest_enemy: Node2D var nearest_distance_squared := target_acquisition_range * target_acquisition_range + if enemy_shot: + return null for candidate in get_tree().get_nodes_in_group("enemies"): if not candidate is Node2D or candidate.is_queued_for_deletion(): continue @@ -170,6 +176,15 @@ func _draw() -> void: if weapon_kind == ItemDefinition.WeaponKind.MISSILE: _draw_missile_smoke() match weapon_kind: + ItemDefinition.WeaponKind.GAMMA: + var radius := 9.0 + sin(_engine_time * 9.0) * 2.0 + draw_circle(Vector2.ZERO, radius * 1.8, Color(0.3, 1.0, 0.1, 0.15)) + draw_circle(Vector2.ZERO, radius, Color(0.35, 0.95, 0.15, 0.8)) + draw_circle(Vector2.ZERO, radius * 0.5, Color(0.86, 1.0, 0.6)) + for index in 8: + var angle := _engine_time * 5.0 + index * TAU / 8.0 + var orbit := Vector2(cos(angle), sin(angle) * 0.65) * (radius + 6.0) + draw_circle(orbit, 2.0, Color(0.7, 1.0, 0.4)) ItemDefinition.WeaponKind.PLASMA: var texture_size := PLASMA_TEXTURE.get_size() * (PLASMA_VISUAL_WIDTH / PLASMA_TEXTURE.get_width()) # The source image points left; projectiles travel along their local +X. @@ -249,6 +264,9 @@ func _spawn_impact_effect(is_explosion: bool) -> void: effect.global_position = global_position get_parent().add_child(effect) var effect_color := Color(1.0, 0.48, 0.22) if is_explosion else Color(1.0, 0.78, 0.38) + if weapon_kind == ItemDefinition.WeaponKind.GAMMA: + effect_color = Color(0.4, 1.0, 0.15) + is_explosion = true effect.call("configure", is_explosion, effect_color) func _explode_at_target(impact_target: Node) -> void: @@ -258,7 +276,7 @@ func _explode_at_target(impact_target: Node) -> void: var query := PhysicsShapeQueryParameters2D.new() query.shape = explosion_shape query.transform = Transform2D(0.0, global_position) - query.collision_mask = 4 # Enemy physics layer 3. + query.collision_mask = 2 if enemy_shot else 4 for result in get_world_2d().direct_space_state.intersect_shape(query, 32): _apply_damage_once(result.get("collider")) @@ -270,6 +288,11 @@ func _apply_damage_once(target: Variant) -> void: return _already_hit[instance_id] = true if target.has_method("take_weapon_hit"): - target.call("take_weapon_hit", damage, is_critical) + target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) elif target.has_method("take_damage"): - target.call("take_damage", damage) + if target is CharacterBody2D: + target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind)) + else: + target.call("take_damage", damage) + if weapon_kind == ItemDefinition.WeaponKind.GAMMA: + preload("res://scripts/game/radiation_status.gd").apply(target, damage * 0.25, not enemy_shot) diff --git a/scripts/items/item_definition.gd b/scripts/items/item_definition.gd index 63028d0..18bae6b 100644 --- a/scripts/items/item_definition.gd +++ b/scripts/items/item_definition.gd @@ -2,7 +2,10 @@ extends Resource class_name ItemDefinition enum Category { WEAPON, SHIELD, ARMOR, REACTOR, ENGINE, ACTIVE_EXTENSION, COMPUTER } -enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE } +enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE, GAMMA } + +func get_damage_type() -> int: + return DamageTypes.for_weapon(weapon_kind) @export var item_id: String = "" @export var display_name: String = "" diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index f31df05..e42305f 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -1,6 +1,7 @@ extends RefCounted const DEFINITIONS := [ + preload("res://resources/items/gamma_cannon.tres"), preload("res://resources/items/plasma_cannon.tres"), preload("res://resources/items/beam_laser.tres"), preload("res://resources/items/machine_gun.tres"), diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index fbf7840..7976710 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -55,7 +55,7 @@ func roll_damage() -> float: func get_energy_cost() -> float: if definition == null or definition.category != ItemDefinition.Category.WEAPON: return 0.0 - if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE]: + if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE, ItemDefinition.WeaponKind.GAMMA]: return 0.0 # Compare this weapon with its starter damage. Captain/global gear buffs are free. var base_lower := roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5) @@ -112,9 +112,12 @@ func get_tooltip_text() -> String: 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) * ENERGY_REGENERATION_BONUS_MULTIPLIER)) if definition.category == ItemDefinition.Category.WEAPON: + lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[definition.get_damage_type()]) lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) + elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: + lines.append("Zasięg: %.0f px • Napromieniowana amunicja: 1 / strzał" % definition.weapon_range) elif definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii") else: @@ -129,4 +132,4 @@ func get_tooltip_text() -> String: return "\n".join(lines) static func stat_name(key: String) -> String: - return str({"missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) + return str({"kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key)) diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 1b5614c..f7f5b40 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -20,6 +20,8 @@ const AMMUNITION_CRAFT_PARTS_COST := 50.0 const AMMUNITION_BUY_AMOUNT := 100 const AMMUNITION_BUY_BASE_COST := 20 +var irradiated_ammunition := 0 + var ammunition: int = STARTING_AMMUNITION var missile_ammunition: int = STARTING_MISSILE_AMMUNITION @@ -45,6 +47,7 @@ func _ready() -> void: _restore_modules(GameState.restart_inventory) ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION)) missile_ammunition = int(GameState.restart_inventory.get("missile_ammunition", STARTING_MISSILE_AMMUNITION)) + irradiated_ammunition = int(GameState.restart_inventory.get("irradiated_ammunition", 0)) GameState.restart_inventory.clear() _remove_extension_addons() _recalculate_ship_stats() @@ -64,6 +67,7 @@ func preserve_for_restart() -> void: "weapon": weapon_slots.duplicate(), "module": module_slots.duplicate(), "extension": extension_slots.duplicate(), + "irradiated_ammunition": irradiated_ammunition, "ammunition": ammunition, "missile_ammunition": missile_ammunition, } @@ -231,7 +235,7 @@ func _recalculate_ship_stats() -> void: for item in weapon_slots + module_slots: if item == null or item.definition == null: continue - for stat_key in ["shield_on_damage_percent", "scanner_range", "max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]: + for stat_key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "shield_on_damage_percent", "scanner_range", "max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]: bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key) ship.call("apply_equipment_bonuses", bonuses) @@ -288,7 +292,7 @@ func _comparison_bonuses(incoming: ItemInstance = null, outgoing: ItemInstance = return bonuses func _add_comparison_bonuses(bonuses: Dictionary, item: ItemInstance) -> void: - for key in ["max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent"]: + for key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent"]: bonuses[key] = float(bonuses.get(key, 0.0)) + item.get_stat_total(key) func _comparison_stats(bonuses: Dictionary) -> Dictionary: @@ -299,9 +303,12 @@ func _comparison_stats(bonuses: Dictionary) -> Dictionary: for data in [["max_energy", "base_max_energy", "Energia", "pkt"], ["scanner_range", "base_scanner_range", "Zasięg skanera", "px"], ["energy_regeneration", "base_energy_regeneration", "Regeneracja energii", "pkt/s"], ["shield_regeneration", "base_shield_regeneration", "Regeneracja tarcz", "pkt/s"]]: stats[data[0]] = {"value": float(ship.get(data[1])) + float(bonuses.get(data[0], 0.0)), "name": data[2], "unit": data[3]} # These stats are bonuses in their own right, not rewards or hit scenarios. - for data in [["damage_percent", "Obrażenia broni"], ["critical_damage_percent", "Obrażenia krytyczne"], ["critical_chance_percent", "Szansa na krytyk"], ["shield_on_damage_percent", "Odzysk tarcz z obrażeń"], ["experience_percent", "Doświadczenie"], ["credits_percent", "Kredyty"]]: + for data in [["kinetic_absorption", "Absorpcja kinetyczna"], ["energy_absorption", "Absorpcja energetyczna"], ["radiation_absorption", "Absorpcja radiacyjna"], ["damage_percent", "Obrażenia broni"], ["critical_damage_percent", "Obrażenia krytyczne"], ["critical_chance_percent", "Szansa na krytyk"], ["shield_on_damage_percent", "Odzysk tarcz z obrażeń"], ["experience_percent", "Doświadczenie"], ["credits_percent", "Kredyty"]]: stats[data[0]] = {"value": float(bonuses.get(data[0], 0.0)), "name": data[1], "unit": "%"} stats["missile_cooldown_reduction"] = {"value": float(bonuses.get("missile_cooldown_reduction", 0.0)), "name": "Skrócenie cooldownu rakiet", "unit": "s"} + var absorption_values := DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0)))) + for index in 3: + stats[DamageTypes.KEYS[index]]["value"] = absorption_values[index] return stats func get_weapon_comparison(item: ItemInstance, slot_index: int) -> Dictionary: @@ -334,3 +341,45 @@ func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: interval = maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) return item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) + +func consume_irradiated_ammunition() -> bool: + if irradiated_ammunition <= 0: + return false + irradiated_ammunition -= 1 + ammunition_changed.emit() + return true + +func can_craft_irradiated_ammunition() -> bool: + return int(GameState.captain.minerals.get("uranium", 0)) >= 1 and GameState.captain.parts >= 10.0 + +func craft_irradiated_ammunition() -> bool: + if not can_craft_irradiated_ammunition(): + return false + GameState.captain.minerals["uranium"] -= 1 + GameState.captain.parts -= 10.0 + irradiated_ammunition += 10 + GameState.captain.emit_changed() + ammunition_changed.emit() + return true + +func can_buy_irradiated_ammunition() -> bool: + return GameState.captain.credits >= 15 * maxi(1, GameState.system_level) + +func buy_irradiated_ammunition() -> bool: + if not can_buy_irradiated_ammunition(): + return false + GameState.captain.credits -= 15 * maxi(1, GameState.system_level) + irradiated_ammunition += 10 + GameState.captain.emit_changed() + ammunition_changed.emit() + return true + +func get_total_dps() -> float: + var total := 0.0 + var bonuses := _comparison_bonuses() + for item in weapon_slots: + var value := _weapon_dps(item, bonuses) + if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: + value *= 1.0 + 0.25 * minf(3.0, item.definition.fire_interval) + total += value + return total diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd index 6a0d5c1..d8cb732 100644 --- a/scripts/levels/exploration_poi.gd +++ b/scripts/levels/exploration_poi.gd @@ -22,7 +22,7 @@ var _asteroid_spin := 0.0 func _ready() -> void: add_to_group("exploration_pois") _rng.seed = reward_seed - _mineral = ["iron", "aluminium", "titanium"][_rng.randi_range(0, 2)] + _mineral = ["iron", "aluminium", "titanium", "uranium"][_rng.randi_range(0, 3)] hull = 20.0 * reward_scale() collision_layer = 4 if kind == "asteroid" else 0 collision_mask = 0 @@ -79,7 +79,7 @@ func _update_label(near: bool) -> void: if kind == "ruin": _label.text = "?" elif kind == "asteroid": - _label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan"}[_mineral], hull] + _label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan", "uranium": "Uran"}[_mineral], hull] else: _label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać") @@ -91,7 +91,7 @@ func _physics_process(_delta: float) -> void: if near and (kind == "ruin" or (kind == "wreck" and Input.is_physical_key_pressed(KEY_E))): claim() -func take_weapon_hit(amount: float, _critical: bool = false) -> void: +func take_weapon_hit(amount: float, _critical: bool = false, _damage_type: int = 0) -> void: take_damage(amount) func take_damage(amount: float) -> void: @@ -114,7 +114,7 @@ func claim() -> bool: if kind == "asteroid": var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount - summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu"}[_mineral]] + summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu"}[_mineral]] elif kind == "ruin": var xp := roundi(_rng.randi_range(10, 20) * scale) GameState.captain.add_rewards(xp, 0) diff --git a/scripts/ui/enemy_drawer.gd b/scripts/ui/enemy_drawer.gd new file mode 100644 index 0000000..fc10c77 --- /dev/null +++ b/scripts/ui/enemy_drawer.gd @@ -0,0 +1,60 @@ +extends Control + +var _panel: PanelContainer +var _label: Label +var _toggle: Button +var _open := false +var _tween: Tween + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + _panel = PanelContainer.new() + var style := StyleBoxFlat.new() + style.bg_color = Color("0b1520f5") + style.border_color = Color("a54e61") + style.set_border_width_all(1) + style.set_content_margin_all(12) + _panel.add_theme_stylebox_override("panel", style) + add_child(_panel) + _label = Label.new() + _label.add_theme_font_size_override("font_size", 12) + _panel.add_child(_label) + _toggle = Button.new() + _toggle.text = "WRÓG ▾" + _toggle.focus_mode = Control.FOCUS_NONE + add_child(_toggle) + _toggle.pressed.connect(_toggle_drawer) + _open = bool(GameState.hud_panel_state.get("enemy_open", false)) + get_viewport().size_changed.connect(_layout) + _layout() + +func _layout() -> void: + position.x = (get_viewport_rect().size.x - 390.0) * 0.5 + _panel.size = Vector2(390, 150) + _panel.position.y = 42.0 if _open else -160.0 + _toggle.position = Vector2(130, 12) + _toggle.size = Vector2(130, 28) + +func _toggle_drawer() -> void: + _open = not _open + GameState.hud_panel_state["enemy_open"] = _open + _toggle.text = "WRÓG ▴" if _open else "WRÓG ▾" + if _tween != null: + _tween.kill() + _tween = create_tween() + _tween.tween_property(_panel, "position:y", 42.0 if _open else -160.0, 0.25).set_trans(Tween.TRANS_CUBIC) + +func _process(_delta: float) -> void: + var enemy: Node = null + for candidate in get_tree().get_nodes_in_group("enemies"): + if not candidate.is_queued_for_deletion() and (enemy == null or int(candidate.loot_tier) < int(enemy.loot_tier)): + enemy = candidate + var profile := DamageTypes.enemy_profile(GameState.system_level, GameState.system_seed) + var kind := int(profile["kind"]) + var names := ["Działko plazmowe", "Działko maszynowe", "Laser", "Rakiety", "Działko gamma"] + _label.text = "WRÓG · UKŁAD %d\n%s · %s\n%s" % [GameState.system_level, names[kind], DamageTypes.NAMES[DamageTypes.for_weapon(kind)], DamageTypes.describe(profile["absorption"])] + if enemy != null: + _label.text += "\nDPS: %.1f (%s)\nTarcze: %.0f / %.0f · bez regeneracji" % [enemy.get_weapon_dps(), ["zwykły", "elita", "boss"][enemy.loot_tier], enemy.shields, enemy.max_shields] + else: + _label.text += "\nUkład oczyszczony" diff --git a/scripts/ui/enemy_drawer.gd.uid b/scripts/ui/enemy_drawer.gd.uid new file mode 100644 index 0000000..274e4ee --- /dev/null +++ b/scripts/ui/enemy_drawer.gd.uid @@ -0,0 +1 @@ +uid://d0ejbqbvdb2kq diff --git a/scripts/ui/flight_cockpit.gd b/scripts/ui/flight_cockpit.gd index e021107..4392461 100644 --- a/scripts/ui/flight_cockpit.gd +++ b/scripts/ui/flight_cockpit.gd @@ -32,6 +32,8 @@ func _draw() -> void: closed.append(outline[0]) draw_polyline(closed, Color("376779"), 1.5, true) draw_line(Vector2(28, 2), Vector2(size.x - 28, 2), CYAN, 2, true) + _text("DPS: %.1f · GAMMA: %d" % [_inventory.get_total_dps(), _inventory.irradiated_ammunition], Vector2(20, 143), size.x - 40, 12, CYAN) + _text(DamageTypes.describe(player.get("absorption")), Vector2(20, 162), size.x - 40, 10, Color("a9d69a")) var cell := (size.x - 40) / 3.0 var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))] var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"] diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 5f63659..91724c9 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -14,7 +14,16 @@ var planet: Node2D var _open_tween: Tween var _missile_button: Button +var _gamma_buy: Button + func _ready() -> void: + _gamma_buy = Button.new() + _gamma_buy.name = "BuyIrradiatedAmmunition" + _gamma_buy.custom_minimum_size.y = 36 + _gamma_buy.add_theme_font_size_override("font_size", 12) + _gamma_buy.focus_mode = Control.FOCUS_NONE + $Center/Panel/Contents.add_child(_gamma_buy) + _gamma_buy.pressed.connect(func() -> void: _inventory.buy_irradiated_ammunition()) next_button.pressed.connect(func() -> void: next_level_requested.emit()) buy_ammunition_button.pressed.connect(_buy_ammunition) reset_skills_button.pressed.connect(_reset_skills) @@ -75,6 +84,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 - 150.0))) func _refresh_ammunition_service() -> void: + if _gamma_buy != null: + _gamma_buy.text = "KUP 10 NAPROMIENIOWANEJ — %d KREDYTÓW" % (15 * maxi(1, GameState.system_level)) + _gamma_buy.disabled = _inventory == null or not _inventory.can_buy_irradiated_ammunition() _missile_button.disabled = _inventory == null or not _inventory.can_buy_missile() _missile_button.text = "KUP +1 RAKIETĘ — %d KREDYTÓW" % (_inventory.get_missile_buy_cost() if _inventory else ShipInventory.MISSILE_BUY_BASE_COST) reset_skills_button.disabled = not GameState.captain.can_reset_skills() diff --git a/scripts/ui/ship_crafting_panel.gd b/scripts/ui/ship_crafting_panel.gd index 8b19d32..1447824 100644 --- a/scripts/ui/ship_crafting_panel.gd +++ b/scripts/ui/ship_crafting_panel.gd @@ -7,6 +7,7 @@ var _details: VBoxContainer var _parts: Label var _stock: Label var _ammo: Button +var _gamma: Button var _missile: Button func _ready() -> void: @@ -36,6 +37,8 @@ func _ready() -> void: _missile = _button("CraftMissile", "+1 RAKIETA · %.0f CZĘŚCI" % ShipInventory.MISSILE_CRAFT_PARTS_COST) _missile.tooltip_text = "Tworzy rakietę do dowolnej wyrzutni." _missile.pressed.connect(func() -> void: _inventory.craft_missile()) + _gamma = _button("CraftGamma", "+10 NAPROMIENIOWANEJ · 1 URAN + 10 CZĘŚCI") + _gamma.pressed.connect(func() -> void: _inventory.craft_irradiated_ammunition()) _details.hide() if bool(GameState.hud_panel_state.get("crafting_expanded", false)): _toggle_expanded() @@ -70,4 +73,6 @@ func _refresh() -> void: _parts.text = "DOSTĘPNE CZĘŚCI: %.1f" % GameState.captain.parts _stock.text = "AMUNICJA: %d / RAKIETY: %d" % [_inventory.ammunition, _inventory.missile_ammunition] _ammo.disabled = not _inventory.can_craft_ammunition() + _gamma.disabled = not _inventory.can_craft_irradiated_ammunition() + _stock.text += "\nNAPROMIENIOWANA: %d · URAN: %d" % [_inventory.irradiated_ammunition, int(GameState.captain.minerals.get("uranium", 0))] _missile.disabled = not _inventory.can_craft_missile() diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd index 2b7b25a..d18d4ba 100644 --- a/scripts/ui/ship_inventory_panel.gd +++ b/scripts/ui/ship_inventory_panel.gd @@ -38,6 +38,7 @@ func _refresh_title() -> void: if _inventory: title_label.text = "MAGAZYN (%d/%d) · Części: %.1f" % [_inventory.get_cargo_used(), CARGO_SIZE, GameState.captain.parts] ammunition_label.text = "AMUNICJA: %d · RAKIETY: %d" % [_inventory.ammunition, _inventory.get_missile_count()] + ammunition_label.text += " · GAMMA: %d" % _inventory.irradiated_ammunition func toggle_drawer() -> void: _set_drawer_open(not _drawer_open) diff --git a/tests/captain/captain_system_test.gd b/tests/captain/captain_system_test.gd index b7a828c..f7a56a2 100644 --- a/tests/captain/captain_system_test.gd +++ b/tests/captain/captain_system_test.gd @@ -10,7 +10,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd") class DamageReceiver: extends Node2D var hits: Array[Dictionary] = [] - func take_weapon_hit(amount: float, critical: bool) -> void: + func take_weapon_hit(amount: float, critical: bool, _damage_type: int = 0) -> void: hits.append({"damage": amount, "critical": critical}) func _ready() -> void: diff --git a/tests/items/affix_database_test.gd b/tests/items/affix_database_test.gd index 7bc1918..5711e35 100644 --- a/tests/items/affix_database_test.gd +++ b/tests/items/affix_database_test.gd @@ -4,12 +4,15 @@ const GENERATOR := preload("res://scripts/items/item_generator.gd") const DATABASE := preload("res://resources/items/affix_database.tres") func _ready() -> void: - assert(DATABASE.affixes.size() == 10) + assert(DATABASE.affixes.size() == 15) for level in [1, 50, 1000]: for tier in [0, 1, 2]: for sample in 50: var item := GENERATOR.generate(level, tier) assert(item.item_level == level) + if item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION: + assert(item.additional_modifiers.is_empty()) + continue assert(item.additional_modifiers.size() >= 1 and item.additional_modifiers.size() <= 5) if tier == 2: assert(item.additional_modifiers.size() >= 4) @@ -18,9 +21,9 @@ func _ready() -> void: assert(modifier.affix.can_roll(item.definition, level)) assert(modifier.affix.affix_id not in ids) ids.append(modifier.affix.affix_id) - var progression := float(level - 1) / 999.0 - assert(modifier.value >= lerpf(modifier.affix.minimum_value_level_1, modifier.affix.minimum_value_level_1000, progression)) - assert(modifier.value <= lerpf(modifier.affix.maximum_value_level_1, modifier.affix.maximum_value_level_1000, progression)) + var bounds := modifier.affix.get_value_range(level) + assert(modifier.value >= bounds.x - 0.0001) + assert(modifier.value <= bounds.y + 0.0001) assert(item.get_tooltip_text().contains(item.additional_modifiers[0].affix.display_name)) var weapon := ItemDefinition.new() diff --git a/tests/items/credit_pickup_test.gd b/tests/items/credit_pickup_test.gd index 882fee4..ef01d69 100644 --- a/tests/items/credit_pickup_test.gd +++ b/tests/items/credit_pickup_test.gd @@ -22,7 +22,7 @@ func _ready() -> void: enemy.set("_player", ship) enemy.call("apply_level_settings", 4, 2) var expected := roundi(BALANCE.KILL_CREDITS[2] * 1.15 * 1.5) - enemy.call("take_damage", enemy.get("max_hull")) + enemy.call("take_damage", 10.0 * (float(enemy.get("max_hull")) + float(enemy.get("max_shields")))) assert(GameState.captain.credits == 0) assert(GameState.captain.level > 1 or GameState.captain.experience > 0) await get_tree().process_frame diff --git a/tests/items/damage_types_test.gd b/tests/items/damage_types_test.gd new file mode 100644 index 0000000..46cfd5e --- /dev/null +++ b/tests/items/damage_types_test.gd @@ -0,0 +1,84 @@ +extends Node2D + +func _ready() -> void: + GameState.start_new_run() + GameState.system_seed = 12345 + assert(DamageTypes.for_weapon(ItemDefinition.WeaponKind.MACHINE_GUN) == DamageTypes.Type.KINETIC) + assert(DamageTypes.for_weapon(ItemDefinition.WeaponKind.MISSILE) == DamageTypes.Type.KINETIC) + assert(DamageTypes.for_weapon(ItemDefinition.WeaponKind.PLASMA) == DamageTypes.Type.ENERGY) + assert(DamageTypes.for_weapon(ItemDefinition.WeaponKind.LASER) == DamageTypes.Type.ENERGY) + assert(DamageTypes.for_weapon(ItemDefinition.WeaponKind.GAMMA) == DamageTypes.Type.RADIATION) + var capped := DamageTypes.capped(Vector3(40, 40, 40)) + assert(capped.is_equal_approx(Vector3(20, 20, 20))) + for level in [1, 500, 1000]: + var profile := DamageTypes.enemy_profile(level, 12345) + assert(profile == DamageTypes.enemy_profile(level, 12345)) + var values: Vector3 = profile["absorption"] + assert(values.x + values.y + values.z <= 60.001) + if level == 1: + assert(is_equal_approx(values.x + values.y + values.z, 2.0)) + 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 inventory: ShipInventory = ship.get_node("ShipInventory") + var module := ItemInstance.new() + module.definition = preload("res://resources/items/hull_plating.tres") + var modifier := ItemStatModifier.new() + modifier.stat_key = "kinetic_absorption" + modifier.value = 20.0 + module.additional_modifiers.append(modifier) + assert(inventory.add_item(module)) + assert(inventory.move_item("cargo", inventory.cargo.find(module), "module", 1)) + assert(is_equal_approx(ship.absorption.x, 20.0)) + ship.shields = 100.0 + ship.take_damage(10.0, false, DamageTypes.Type.KINETIC) + assert(is_equal_approx(ship.shields, 92.0)) + assert(inventory.move_item("module", 1, "cargo", inventory.cargo.find(null))) + assert(ship.absorption == Vector3.ZERO) + GameState.captain.parts = 10 + GameState.captain.minerals["uranium"] = 1 + assert(inventory.craft_irradiated_ammunition()) + assert(inventory.irradiated_ammunition == 10) + assert(not inventory.craft_irradiated_ammunition()) + assert(inventory.consume_irradiated_ammunition()) + GameState.captain.credits = 15 + assert(inventory.buy_irradiated_ammunition()) + assert(inventory.irradiated_ammunition == 19) + inventory.preserve_for_restart() + assert(GameState.restart_inventory["irradiated_ammunition"] == 19) + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + enemy.apply_level_settings(1, 0) + add_child(enemy) + enemy.set_physics_process(false) + enemy.absorption = Vector3(20, 0, 0) + enemy.shields = 10 + enemy.hull = 100 + enemy.take_damage(20, false, DamageTypes.Type.KINETIC) + assert(is_equal_approx(enemy.shields, 0)) + assert(is_equal_approx(enemy.hull, 94)) + enemy.take_damage(10, false, DamageTypes.Type.ENERGY) + assert(is_equal_approx(enemy.hull, 84)) + var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + projectile.weapon_kind = ItemDefinition.WeaponKind.GAMMA + projectile.damage = 8 + add_child(projectile) + projectile.set_physics_process(false) + projectile._apply_damage_once(enemy) + assert(is_equal_approx(enemy.hull, 76)) + var status: Node = enemy.get_node("RadiationStatus") + status.set_physics_process(false) + for index in 6: + status._physics_process(0.5) + assert(is_equal_approx(enemy.hull, 70)) + assert(is_equal_approx(status.remaining, 0)) + assert(enemy.get_node("Sprite2D").modulate == Color.WHITE) + for kind in 5: + enemy.weapon_kind = kind + enemy._fire_plasma(Vector2.RIGHT) + for key in ["kinetic", "energy", "radiation"]: + var affix: ItemAffixDefinition = load("res://resources/items/affixes/%s_absorption.tres" % key) + assert(is_equal_approx(affix.roll_value(1), 1.0)) + assert(preload("res://resources/items/gamma_cannon.tres") in preload("res://scripts/items/item_generator.gd").DEFINITIONS) + print("DAMAGE_TYPES_TEST: PASS (types, caps, profiles, crafting, purchase, persistence, shield overflow, radiation duration)") + get_tree().quit() diff --git a/tests/items/damage_types_test.gd.uid b/tests/items/damage_types_test.gd.uid new file mode 100644 index 0000000..b561158 --- /dev/null +++ b/tests/items/damage_types_test.gd.uid @@ -0,0 +1 @@ +uid://b2tol3o50xrk7 diff --git a/tests/items/damage_types_test.tscn b/tests/items/damage_types_test.tscn new file mode 100644 index 0000000..f96bb34 --- /dev/null +++ b/tests/items/damage_types_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/damage_types_test.gd" id="1"] +[node name="DamageTypesTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/machine_gun_shield_recovery_test.gd b/tests/items/machine_gun_shield_recovery_test.gd index d85d82c..7592fe4 100644 --- a/tests/items/machine_gun_shield_recovery_test.gd +++ b/tests/items/machine_gun_shield_recovery_test.gd @@ -49,6 +49,7 @@ func _ready() -> void: enemy.set("_player", ship) enemy.set("max_hull", 10000.0) enemy.set("hull", 10000.0) + enemy.set("shields", 0.0) var controller := AimedGun.new() ship.add_child(controller) await get_tree().physics_frame diff --git a/tests/items/shield_on_damage_test.gd b/tests/items/shield_on_damage_test.gd index 5837b61..64aa374 100644 --- a/tests/items/shield_on_damage_test.gd +++ b/tests/items/shield_on_damage_test.gd @@ -73,4 +73,5 @@ func _enemy(ship: CharacterBody2D, hp: float) -> CharacterBody2D: enemy.set_physics_process(false) enemy.set("_player", ship) enemy.set("hull", hp) + enemy.set("shields", 0.0) return enemy diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd index df7e9e5..f116a5d 100644 --- a/tests/items/weapon_damage_test.gd +++ b/tests/items/weapon_damage_test.gd @@ -32,7 +32,7 @@ func _ready() -> void: addon.stat_key = "damage_percent" addon.value = 10.0 stronger.additional_modifiers.append(addon) - if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE]: + if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE, ItemDefinition.WeaponKind.GAMMA]: assert(stronger.get_energy_cost() == 0.0) else: assert(stronger.get_energy_cost() > cost_before_addon) diff --git a/tests/levels/system_lifecycle_test.gd b/tests/levels/system_lifecycle_test.gd index 817aa65..de767fb 100644 --- a/tests/levels/system_lifecycle_test.gd +++ b/tests/levels/system_lifecycle_test.gd @@ -210,6 +210,9 @@ func _assert_boss_and_health(game: Node2D) -> void: assert(boss.get_node("CollisionShape2D").scale == Vector2(2, 2)) var bar: Node2D = boss.get_node("HealthBar") assert(is_equal_approx(bar.get_health_ratio(), 1.0)) + # Isolate the hull bar from the new shield and absorption layers. + boss.shields = 0.0 + boss.absorption = Vector3.ZERO boss.take_damage(boss.max_hull * 0.5) assert(is_equal_approx(bar.get_health_ratio(), 0.5)) boss.rotation = 1.2