Zmiany w brocniah i dodanie ebsorcji

This commit is contained in:
Tomasz Boruc 2026-10-13 20:10:16 +02:00
parent 24a58b60db
commit f12ed2f67e
41 changed files with 548 additions and 45 deletions

1
assets/icons/gamma.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><defs><radialGradient id="g"><stop stop-color="#edffd0"/><stop offset=".5" stop-color="#80ff45"/><stop offset="1" stop-color="#174823"/></radialGradient></defs><path d="M27 80V47L34 24H62L69 47V80Z" fill="#293e46" stroke="#95b7ad" stroke-width="3"/><path d="M38 60V18H58V60" fill="#11241a" stroke="#59d44c" stroke-width="3"/><circle cx="48" cy="25" r="17" fill="url(#g)"/><ellipse cx="48" cy="25" rx="25" ry="10" fill="none" stroke="#b7ff72" stroke-width="2" transform="rotate(-35 48 25)"/><circle cx="65" cy="10" r="3" fill="#e0ffc3"/><path d="M34 68H62M34 74H62" stroke="#7eff4f" stroke-width="3"/></svg>

After

Width:  |  Height:  |  Size: 689 B

View File

@ -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

View File

@ -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_database.gd" id="1_database"]
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"] [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/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] [resource]
script = ExtResource("1_database") 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")])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -7,7 +7,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
@export var experience: int = 0 @export var experience: int = 0
@export var credits: int = 0 @export var credits: int = 0
@export var parts: float = 0.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 learning_points: int = 0
@export var skills: Dictionary = { @export var skills: Dictionary = {
"piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0, "piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0,

View File

@ -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]

View File

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

View File

@ -46,5 +46,7 @@ func _draw() -> void:
var background := Rect2(-width * 0.5, -3.0, width, 6.0) var background := Rect2(-width * 0.5, -3.0, width, 6.0)
draw_rect(background.grow(1.0), Color("08111dee")) draw_rect(background.grow(1.0), Color("08111dee"))
draw_rect(background, Color("39232bee")) draw_rect(background, Color("39232bee"))
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() var color := _tier_color()
draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color) draw_rect(Rect2(background.position, Vector2(width * get_health_ratio(), 6.0)), color)

View File

@ -49,5 +49,8 @@ func _hit_target(target: Variant) -> void:
get_parent().add_child(effect) get_parent().add_child(effect)
effect.call("configure", false, Color(1.0, 0.32, 0.38)) effect.call("configure", false, Color(1.0, 0.32, 0.38))
if target is Node and target.has_method("take_damage"): 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() queue_free()

View File

@ -2,7 +2,6 @@ extends CharacterBody2D
const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn") const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn")
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.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 WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd")
const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd") const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd")
const LOOT_PICKUP := preload("res://scripts/items/loot_pickup.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_cooldown := 1.5
@export var weapon_damage := 8.0 @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 hull := 50.0
var _player: CharacterBody2D var _player: CharacterBody2D
var _activated := false var _activated := false
@ -29,6 +34,11 @@ var _weapon_timer := 0.0
func _ready() -> void: func _ready() -> void:
hull = max_hull hull = max_hull
_sprite = $Sprite2D _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") add_to_group("enemies")
_weapon_timer = randf_range(0.2, weapon_cooldown) _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) 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 max_hull = 50.0 * power * hull_multiplier * combat_multiplier
hull = max_hull 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 weapon_damage = 8.0 * power * damage_bonus * combat_multiplier
approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier approach_speed = 62.0 * (1.0 + 0.0005 * (clampi(level, 1, 1000) - 1)) * boss_multiplier
approach_acceleration = 85.0 * boss_multiplier approach_acceleration = 85.0 * boss_multiplier
@ -50,6 +65,9 @@ func apply_level_settings(level: int, tier: int) -> void:
weapon_range = 680.0 * boss_multiplier weapon_range = 680.0 * boss_multiplier
weapon_cooldown = maxf(0.55, 1.5 / (1.0 + 0.001 * (clampi(level, 1, 1000) - 1))) / 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 $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: func configure(player: CharacterBody2D, ship_texture: Texture2D) -> void:
_player = player _player = player
@ -98,31 +116,63 @@ func _check_player_collision() -> void:
_player.call("register_enemy_collision", global_position) _player.call("register_enemy_collision", global_position)
func _fire_plasma(direction: Vector2) -> void: func _fire_plasma(direction: Vector2) -> void:
var projectile := PLASMA_PROJECTILE_SCENE.instantiate() as Area2D var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
projectile.call("configure", global_position + direction * (get_visual_size() * 0.5), direction, weapon_damage) if weapon_kind == ItemDefinition.WeaponKind.LASER:
get_parent().add_child(projectile) var endpoint := muzzle + direction * weapon_range
# Relative to the Plasma bus: enemy shots are 4 dB quieter than player shots. var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 3, [get_rid()])
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, ItemDefinition.WeaponKind.PLASMA, -4.0) 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: func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTypes.Type.KINETIC) -> 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:
if is_queued_for_deletion() or hull <= 0.0: if is_queued_for_deletion() or hull <= 0.0:
return return
hull = maxf(0.0, hull - maxf(0.0, amount)) var before := hull + shields
_show_damage_popup(amount, critical) 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: if hull <= 0.0:
_reward_captain() _reward_captain()
_drop_loot() _drop_loot()
_spawn_destruction_effect() _spawn_destruction_effect()
queue_free() 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: func _reward_captain() -> void:
if is_instance_valid(_player): 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) var reward := GameState.captain.reward_enemy(GameState.system_level, loot_tier, float(_player.get("experience_multiplier")), float(_player.get("credits_multiplier")), false)

View File

@ -40,6 +40,10 @@ func _ready() -> void:
# The display font has unusually tall vertical metrics; keep debug rows compact. # 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)))) debug_info.add_theme_constant_override("line_spacing", mini(0, 13 - ceili(debug_info.get_theme_font("font").get_height(10))))
_create_sidebar() _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 = Control.new()
_inventory_clip.name = "InventoryDrawer" _inventory_clip.name = "InventoryDrawer"
_inventory_clip.clip_contents = true _inventory_clip.clip_contents = true
@ -154,7 +158,7 @@ func _layout_sidebar() -> void:
_sidebar.scale = Vector2.ONE * EQUIPMENT_UI_SCALE _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))) _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.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) _cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12)
_layout_debug() _layout_debug()
_sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 44) _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 44)

View File

@ -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

View File

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

View File

@ -35,6 +35,7 @@ var shields := 100.0
var energy := 100.0 var energy := 100.0
var energy_regeneration := 5.0 var energy_regeneration := 5.0
var shield_regeneration := 0.0 var shield_regeneration := 0.0
var absorption := Vector3.ZERO
var damage_multiplier := 1.0 var damage_multiplier := 1.0
var experience_multiplier := 1.0 var experience_multiplier := 1.0
var credits_multiplier := 1.0 var credits_multiplier := 1.0
@ -69,6 +70,7 @@ func _enter_tree() -> void:
base_max_speed = float(chassis["speed"]) base_max_speed = float(chassis["speed"])
func apply_equipment_bonuses(bonuses: Dictionary) -> void: 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_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) 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)) 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): if not InputMap.action_has_event(action_name, event):
InputMap.action_add_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: if _is_destroyed or is_docked or amount <= 0.0:
return 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"]: if hit["evaded"]:
var flash := Node2D.new() var flash := Node2D.new()
flash.set_script(COMBAT_FLASH) flash.set_script(COMBAT_FLASH)

View File

@ -2,6 +2,7 @@ extends RefCounted
class_name WeaponAudio class_name WeaponAudio
const SOUND_PATHS := { const SOUND_PATHS := {
ItemDefinition.WeaponKind.GAMMA: "res://assets/sounds/effects/plasma.mp3",
ItemDefinition.WeaponKind.PLASMA: "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.MACHINE_GUN: "res://assets/sounds/effects/machine_gun.mp3",
ItemDefinition.WeaponKind.LASER: "res://assets/sounds/effects/laser.mp3", ItemDefinition.WeaponKind.LASER: "res://assets/sounds/effects/laser.mp3",
@ -11,6 +12,7 @@ const SOUND_PATHS := {
static var _streams: Dictionary = {} static var _streams: Dictionary = {}
const BUS_NAMES := { const BUS_NAMES := {
ItemDefinition.WeaponKind.GAMMA: &"Plasma",
ItemDefinition.WeaponKind.PLASMA: &"Plasma", ItemDefinition.WeaponKind.PLASMA: &"Plasma",
ItemDefinition.WeaponKind.MACHINE_GUN: &"MachineGun", ItemDefinition.WeaponKind.MACHINE_GUN: &"MachineGun",
ItemDefinition.WeaponKind.LASER: &"Laser", ItemDefinition.WeaponKind.LASER: &"Laser",

View File

@ -205,6 +205,8 @@ func _muzzle_position() -> Vector2:
func _consume_shot_resource(item: ItemInstance) -> bool: func _consume_shot_resource(item: ItemInstance) -> bool:
if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:
return inventory.consume_ammunition() return inventory.consume_ammunition()
if item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA:
return inventory.consume_irradiated_ammunition()
return _consume_energy(item.get_energy_cost()) return _consume_energy(item.get_energy_cost())
func _consume_energy(amount: float) -> bool: 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: func _apply_damage(target: Variant, damage: float, critical: bool = false) -> void:
if target is Node and target.has_method("take_weapon_hit"): 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"): elif target is Node and target.has_method("take_damage"):
target.call("take_damage", damage) target.call("take_damage", damage)

View File

@ -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_TEXTURE := preload("res://assets/other/bullet.png")
const PLASMA_VISUAL_WIDTH := 24.0 const PLASMA_VISUAL_WIDTH := 24.0
var enemy_shot := false
var direction := Vector2.UP var direction := Vector2.UP
var projectile_speed := 600.0 var projectile_speed := 600.0
var damage := 1.0 var damage := 1.0
@ -124,6 +125,9 @@ func _physics_process(delta: float) -> void:
queue_free() queue_free()
func _process(delta: float) -> void: func _process(delta: float) -> void:
if weapon_kind == ItemDefinition.WeaponKind.GAMMA:
_engine_time += delta
queue_redraw()
if weapon_kind == ItemDefinition.WeaponKind.MISSILE: if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
_engine_time += delta _engine_time += delta
_smoke_emission_timer += delta _smoke_emission_timer += delta
@ -157,6 +161,8 @@ func _update_smoke_puffs(delta: float) -> void:
func _find_enemy_in_acquisition_range() -> Node2D: func _find_enemy_in_acquisition_range() -> Node2D:
var nearest_enemy: Node2D var nearest_enemy: Node2D
var nearest_distance_squared := target_acquisition_range * target_acquisition_range 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"): for candidate in get_tree().get_nodes_in_group("enemies"):
if not candidate is Node2D or candidate.is_queued_for_deletion(): if not candidate is Node2D or candidate.is_queued_for_deletion():
continue continue
@ -170,6 +176,15 @@ func _draw() -> void:
if weapon_kind == ItemDefinition.WeaponKind.MISSILE: if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
_draw_missile_smoke() _draw_missile_smoke()
match weapon_kind: 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: ItemDefinition.WeaponKind.PLASMA:
var texture_size := PLASMA_TEXTURE.get_size() * (PLASMA_VISUAL_WIDTH / PLASMA_TEXTURE.get_width()) 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. # 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 effect.global_position = global_position
get_parent().add_child(effect) 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) 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) effect.call("configure", is_explosion, effect_color)
func _explode_at_target(impact_target: Node) -> void: func _explode_at_target(impact_target: Node) -> void:
@ -258,7 +276,7 @@ func _explode_at_target(impact_target: Node) -> void:
var query := PhysicsShapeQueryParameters2D.new() var query := PhysicsShapeQueryParameters2D.new()
query.shape = explosion_shape query.shape = explosion_shape
query.transform = Transform2D(0.0, global_position) 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): for result in get_world_2d().direct_space_state.intersect_shape(query, 32):
_apply_damage_once(result.get("collider")) _apply_damage_once(result.get("collider"))
@ -270,6 +288,11 @@ func _apply_damage_once(target: Variant) -> void:
return return
_already_hit[instance_id] = true _already_hit[instance_id] = true
if target.has_method("take_weapon_hit"): 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"): 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)

View File

@ -2,7 +2,10 @@ extends Resource
class_name ItemDefinition class_name ItemDefinition
enum Category { WEAPON, SHIELD, ARMOR, REACTOR, ENGINE, ACTIVE_EXTENSION, COMPUTER } 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 item_id: String = ""
@export var display_name: String = "" @export var display_name: String = ""

View File

@ -1,6 +1,7 @@
extends RefCounted extends RefCounted
const DEFINITIONS := [ const DEFINITIONS := [
preload("res://resources/items/gamma_cannon.tres"),
preload("res://resources/items/plasma_cannon.tres"), preload("res://resources/items/plasma_cannon.tres"),
preload("res://resources/items/beam_laser.tres"), preload("res://resources/items/beam_laser.tres"),
preload("res://resources/items/machine_gun.tres"), preload("res://resources/items/machine_gun.tres"),

View File

@ -55,7 +55,7 @@ func roll_damage() -> float:
func get_energy_cost() -> float: func get_energy_cost() -> float:
if definition == null or definition.category != ItemDefinition.Category.WEAPON: if definition == null or definition.category != ItemDefinition.Category.WEAPON:
return 0.0 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 return 0.0
# Compare this weapon with its starter damage. Captain/global gear buffs are free. # 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) 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: 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)) 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("Typ obrażeń: %s" % DamageTypes.NAMES[definition.get_damage_type()])
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:
lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) 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: elif definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE:
lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii") lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii")
else: else:
@ -129,4 +132,4 @@ func get_tooltip_text() -> String:
return "\n".join(lines) return "\n".join(lines)
static func stat_name(key: String) -> String: 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))

View File

@ -20,6 +20,8 @@ const AMMUNITION_CRAFT_PARTS_COST := 50.0
const AMMUNITION_BUY_AMOUNT := 100 const AMMUNITION_BUY_AMOUNT := 100
const AMMUNITION_BUY_BASE_COST := 20 const AMMUNITION_BUY_BASE_COST := 20
var irradiated_ammunition := 0
var ammunition: int = STARTING_AMMUNITION var ammunition: int = STARTING_AMMUNITION
var missile_ammunition: int = STARTING_MISSILE_AMMUNITION var missile_ammunition: int = STARTING_MISSILE_AMMUNITION
@ -45,6 +47,7 @@ func _ready() -> void:
_restore_modules(GameState.restart_inventory) _restore_modules(GameState.restart_inventory)
ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION)) ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION))
missile_ammunition = int(GameState.restart_inventory.get("missile_ammunition", STARTING_MISSILE_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() GameState.restart_inventory.clear()
_remove_extension_addons() _remove_extension_addons()
_recalculate_ship_stats() _recalculate_ship_stats()
@ -64,6 +67,7 @@ func preserve_for_restart() -> void:
"weapon": weapon_slots.duplicate(), "weapon": weapon_slots.duplicate(),
"module": module_slots.duplicate(), "module": module_slots.duplicate(),
"extension": extension_slots.duplicate(), "extension": extension_slots.duplicate(),
"irradiated_ammunition": irradiated_ammunition,
"ammunition": ammunition, "ammunition": ammunition,
"missile_ammunition": missile_ammunition, "missile_ammunition": missile_ammunition,
} }
@ -231,7 +235,7 @@ func _recalculate_ship_stats() -> void:
for item in weapon_slots + module_slots: for item in weapon_slots + module_slots:
if item == null or item.definition == null: if item == null or item.definition == null:
continue 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) bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key)
ship.call("apply_equipment_bonuses", bonuses) ship.call("apply_equipment_bonuses", bonuses)
@ -288,7 +292,7 @@ func _comparison_bonuses(incoming: ItemInstance = null, outgoing: ItemInstance =
return bonuses return bonuses
func _add_comparison_bonuses(bonuses: Dictionary, item: ItemInstance) -> void: 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) bonuses[key] = float(bonuses.get(key, 0.0)) + item.get_stat_total(key)
func _comparison_stats(bonuses: Dictionary) -> Dictionary: 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"]]: 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]} 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. # 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[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"} 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 return stats
func get_weapon_comparison(item: ItemInstance, slot_index: int) -> Dictionary: 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: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE:
interval = maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) 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) 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

View File

@ -22,7 +22,7 @@ var _asteroid_spin := 0.0
func _ready() -> void: func _ready() -> void:
add_to_group("exploration_pois") add_to_group("exploration_pois")
_rng.seed = reward_seed _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() hull = 20.0 * reward_scale()
collision_layer = 4 if kind == "asteroid" else 0 collision_layer = 4 if kind == "asteroid" else 0
collision_mask = 0 collision_mask = 0
@ -79,7 +79,7 @@ func _update_label(near: bool) -> void:
if kind == "ruin": if kind == "ruin":
_label.text = "?" _label.text = "?"
elif kind == "asteroid": 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: else:
_label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać") _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))): if near and (kind == "ruin" or (kind == "wreck" and Input.is_physical_key_pressed(KEY_E))):
claim() 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) take_damage(amount)
func take_damage(amount: float) -> void: func take_damage(amount: float) -> void:
@ -114,7 +114,7 @@ func claim() -> bool:
if kind == "asteroid": if kind == "asteroid":
var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale))
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount 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": elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale) var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0) GameState.captain.add_rewards(xp, 0)

View File

@ -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"

View File

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

View File

@ -32,6 +32,8 @@ func _draw() -> void:
closed.append(outline[0]) closed.append(outline[0])
draw_polyline(closed, Color("376779"), 1.5, true) draw_polyline(closed, Color("376779"), 1.5, true)
draw_line(Vector2(28, 2), Vector2(size.x - 28, 2), CYAN, 2, 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 cell := (size.x - 40) / 3.0
var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))] var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))]
var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"] var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"]

View File

@ -14,7 +14,16 @@ var planet: Node2D
var _open_tween: Tween var _open_tween: Tween
var _missile_button: Button var _missile_button: Button
var _gamma_buy: Button
func _ready() -> void: 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()) 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) 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))) 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: 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.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) _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() reset_skills_button.disabled = not GameState.captain.can_reset_skills()

View File

@ -7,6 +7,7 @@ var _details: VBoxContainer
var _parts: Label var _parts: Label
var _stock: Label var _stock: Label
var _ammo: Button var _ammo: Button
var _gamma: Button
var _missile: Button var _missile: Button
func _ready() -> void: func _ready() -> void:
@ -36,6 +37,8 @@ func _ready() -> void:
_missile = _button("CraftMissile", "+1 RAKIETA · %.0f CZĘŚCI" % ShipInventory.MISSILE_CRAFT_PARTS_COST) _missile = _button("CraftMissile", "+1 RAKIETA · %.0f CZĘŚCI" % ShipInventory.MISSILE_CRAFT_PARTS_COST)
_missile.tooltip_text = "Tworzy rakietę do dowolnej wyrzutni." _missile.tooltip_text = "Tworzy rakietę do dowolnej wyrzutni."
_missile.pressed.connect(func() -> void: _inventory.craft_missile()) _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() _details.hide()
if bool(GameState.hud_panel_state.get("crafting_expanded", false)): if bool(GameState.hud_panel_state.get("crafting_expanded", false)):
_toggle_expanded() _toggle_expanded()
@ -70,4 +73,6 @@ func _refresh() -> void:
_parts.text = "DOSTĘPNE CZĘŚCI: %.1f" % GameState.captain.parts _parts.text = "DOSTĘPNE CZĘŚCI: %.1f" % GameState.captain.parts
_stock.text = "AMUNICJA: %d / RAKIETY: %d" % [_inventory.ammunition, _inventory.missile_ammunition] _stock.text = "AMUNICJA: %d / RAKIETY: %d" % [_inventory.ammunition, _inventory.missile_ammunition]
_ammo.disabled = not _inventory.can_craft_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() _missile.disabled = not _inventory.can_craft_missile()

View File

@ -38,6 +38,7 @@ func _refresh_title() -> void:
if _inventory: if _inventory:
title_label.text = "MAGAZYN (%d/%d) · Części: %.1f" % [_inventory.get_cargo_used(), CARGO_SIZE, GameState.captain.parts] 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 = "AMUNICJA: %d · RAKIETY: %d" % [_inventory.ammunition, _inventory.get_missile_count()]
ammunition_label.text += " · GAMMA: %d" % _inventory.irradiated_ammunition
func toggle_drawer() -> void: func toggle_drawer() -> void:
_set_drawer_open(not _drawer_open) _set_drawer_open(not _drawer_open)

View File

@ -10,7 +10,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
class DamageReceiver: class DamageReceiver:
extends Node2D extends Node2D
var hits: Array[Dictionary] = [] 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}) hits.append({"damage": amount, "critical": critical})
func _ready() -> void: func _ready() -> void:

View File

@ -4,12 +4,15 @@ const GENERATOR := preload("res://scripts/items/item_generator.gd")
const DATABASE := preload("res://resources/items/affix_database.tres") const DATABASE := preload("res://resources/items/affix_database.tres")
func _ready() -> void: func _ready() -> void:
assert(DATABASE.affixes.size() == 10) assert(DATABASE.affixes.size() == 15)
for level in [1, 50, 1000]: for level in [1, 50, 1000]:
for tier in [0, 1, 2]: for tier in [0, 1, 2]:
for sample in 50: for sample in 50:
var item := GENERATOR.generate(level, tier) var item := GENERATOR.generate(level, tier)
assert(item.item_level == level) 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) assert(item.additional_modifiers.size() >= 1 and item.additional_modifiers.size() <= 5)
if tier == 2: if tier == 2:
assert(item.additional_modifiers.size() >= 4) assert(item.additional_modifiers.size() >= 4)
@ -18,9 +21,9 @@ func _ready() -> void:
assert(modifier.affix.can_roll(item.definition, level)) assert(modifier.affix.can_roll(item.definition, level))
assert(modifier.affix.affix_id not in ids) assert(modifier.affix.affix_id not in ids)
ids.append(modifier.affix.affix_id) ids.append(modifier.affix.affix_id)
var progression := float(level - 1) / 999.0 var bounds := modifier.affix.get_value_range(level)
assert(modifier.value >= lerpf(modifier.affix.minimum_value_level_1, modifier.affix.minimum_value_level_1000, progression)) assert(modifier.value >= bounds.x - 0.0001)
assert(modifier.value <= lerpf(modifier.affix.maximum_value_level_1, modifier.affix.maximum_value_level_1000, progression)) assert(modifier.value <= bounds.y + 0.0001)
assert(item.get_tooltip_text().contains(item.additional_modifiers[0].affix.display_name)) assert(item.get_tooltip_text().contains(item.additional_modifiers[0].affix.display_name))
var weapon := ItemDefinition.new() var weapon := ItemDefinition.new()

View File

@ -22,7 +22,7 @@ func _ready() -> void:
enemy.set("_player", ship) enemy.set("_player", ship)
enemy.call("apply_level_settings", 4, 2) enemy.call("apply_level_settings", 4, 2)
var expected := roundi(BALANCE.KILL_CREDITS[2] * 1.15 * 1.5) 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.credits == 0)
assert(GameState.captain.level > 1 or GameState.captain.experience > 0) assert(GameState.captain.level > 1 or GameState.captain.experience > 0)
await get_tree().process_frame await get_tree().process_frame

View File

@ -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()

View File

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

View File

@ -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")

View File

@ -49,6 +49,7 @@ func _ready() -> void:
enemy.set("_player", ship) enemy.set("_player", ship)
enemy.set("max_hull", 10000.0) enemy.set("max_hull", 10000.0)
enemy.set("hull", 10000.0) enemy.set("hull", 10000.0)
enemy.set("shields", 0.0)
var controller := AimedGun.new() var controller := AimedGun.new()
ship.add_child(controller) ship.add_child(controller)
await get_tree().physics_frame await get_tree().physics_frame

View File

@ -73,4 +73,5 @@ func _enemy(ship: CharacterBody2D, hp: float) -> CharacterBody2D:
enemy.set_physics_process(false) enemy.set_physics_process(false)
enemy.set("_player", ship) enemy.set("_player", ship)
enemy.set("hull", hp) enemy.set("hull", hp)
enemy.set("shields", 0.0)
return enemy return enemy

View File

@ -32,7 +32,7 @@ func _ready() -> void:
addon.stat_key = "damage_percent" addon.stat_key = "damage_percent"
addon.value = 10.0 addon.value = 10.0
stronger.additional_modifiers.append(addon) 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) assert(stronger.get_energy_cost() == 0.0)
else: else:
assert(stronger.get_energy_cost() > cost_before_addon) assert(stronger.get_energy_cost() > cost_before_addon)

View File

@ -210,6 +210,9 @@ func _assert_boss_and_health(game: Node2D) -> void:
assert(boss.get_node("CollisionShape2D").scale == Vector2(2, 2)) assert(boss.get_node("CollisionShape2D").scale == Vector2(2, 2))
var bar: Node2D = boss.get_node("HealthBar") var bar: Node2D = boss.get_node("HealthBar")
assert(is_equal_approx(bar.get_health_ratio(), 1.0)) 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) boss.take_damage(boss.max_hull * 0.5)
assert(is_equal_approx(bar.get_health_ratio(), 0.5)) assert(is_equal_approx(bar.get_health_ratio(), 0.5))
boss.rotation = 1.2 boss.rotation = 1.2