Zmiany tubylców

This commit is contained in:
Tomasz Boruc 2026-10-17 21:28:08 +02:00
parent a74319d01e
commit 72925fd557
25 changed files with 363 additions and 55 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdy2tv4him7u4"
path="res://.godot/imported/planet_services_preview.png-30cacc45a7eb1c9740376c061502ca8d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://artifacts/planet_services_preview.png"
dest_files=["res://.godot/imported/planet_services_preview.png-30cacc45a7eb1c9740376c061502ca8d.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

View File

@ -7,7 +7,7 @@ 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ę."
description = "Radiacyjna kula. Napromieniowanie: 10 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"

View File

@ -3,16 +3,16 @@
[ext_resource type="Script" path="res://scripts/ui/planet_services_menu.gd" id="1_services"]
[sub_resource type="StyleBoxFlat" id="PanelStyle"]
bg_color = Color(0.045, 0.075, 0.11, 0.64)
bg_color = Color(0.025, 0.045, 0.065, 0.94)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.28, 0.63, 0.54, 1)
content_margin_left = 14.0
content_margin_top = 14.0
content_margin_right = 14.0
content_margin_bottom = 14.0
content_margin_left = 10.0
content_margin_top = 10.0
content_margin_right = 10.0
content_margin_bottom = 10.0
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_left = 8
@ -21,7 +21,7 @@ corner_radius_bottom_right = 8
[node name="PlanetServicesMenu" type="Control"]
visible = false
layout_mode = 0
offset_right = 350.0
offset_right = 320.0
offset_bottom = 250.0
mouse_filter = 2
script = ExtResource("1_services")
@ -34,18 +34,18 @@ anchor_bottom = 1.0
mouse_filter = 2
[node name="Panel" type="PanelContainer" parent="Center"]
custom_minimum_size = Vector2(350, 250)
custom_minimum_size = Vector2(320, 0)
layout_mode = 2
theme_override_styles/panel = SubResource("PanelStyle")
[node name="Contents" type="VBoxContainer" parent="Center/Panel"]
layout_mode = 2
theme_override_constants/separation = 12
theme_override_constants/separation = 7
[node name="Title" type="Label" parent="Center/Panel/Contents"]
layout_mode = 2
theme_override_colors/font_color = Color(0.47, 0.91, 0.74, 1)
theme_override_font_sizes/font_size = 17
theme_override_font_sizes/font_size = 13
text = "USŁUGI TUBYLCÓW"
horizontal_alignment = 1

View File

@ -13,7 +13,7 @@ const DATA := {
"tactical_maneuver": {"name": "Manewr taktyczny", "active": true, "values": [0.30, 0.60, 1.00], "color": "b9a0ef"},
"efficient_magazine": {"name": "Oszczędny magazynek", "active": false, "values": [0.10, 0.20, 0.50], "color": "a9d69a"},
"miner": {"name": "Górnik", "active": false, "values": [3, 6, 10], "color": "e5ac76"},
"power_redirect": {"name": "Przekierowanie zasilania", "active": true, "values": [0.50, 0.90, 1.50], "color": "f28b6e"},
"power_redirect": {"name": "Przekierowanie zasilania", "active": true, "values": [1.00, 1.80, 3.00], "color": "f28b6e"},
}
static func value(skill: String, rank: int) -> float:
@ -22,7 +22,7 @@ static func value(skill: String, rank: int) -> float:
return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1])
static func duration(skill: String) -> float:
return 15.0 if skill == "reactor_overload" else DURATION
return 15.0 if skill == "reactor_overload" else (7.0 if skill == "power_redirect" else DURATION)
static func icon(skill: String) -> Texture2D:
return load("res://assets/icons/captain/%s.png" % skill) as Texture2D
@ -47,5 +47,5 @@ static func description(skill: String, rank: int) -> String:
"miner":
return "+%d szt. surowca z każdej zniszczonej asteroidy." % roundi(value(skill, rank))
"power_redirect":
return "Regeneracja energii +%d%% przez 5 sekund. Zużywa wszystkie tarcze i blokuje ich odzysk na czas działania. Wymaga min. 25%% tarcz." % amount
return "Regeneracja energii +%d%% przez 7 sekund. Zużywa wszystkie tarcze i blokuje ich odzysk na czas działania. Wymaga min. 25%% tarcz." % amount
return ""

View File

@ -42,9 +42,18 @@ var _weapon_timer := 0.0
var _slow_remaining := 0.0
var _combat_target: Node2D
var virus_controlled := false
var _virus_infection_attempted := false
var _reward_claimed := false
func roll_virus_infection() -> bool:
if loot_tier == 2 or _virus_infection_attempted:
return false
_virus_infection_attempted = true
return randf() < 0.5
func set_virus_controlled(value: bool) -> void:
if value and loot_tier == 2:
return
if virus_controlled == value or is_queued_for_deletion():
return
virus_controlled = value
@ -246,9 +255,21 @@ func select_combat_target() -> Node2D:
closest_distance = distance
if closest != null:
return closest
if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")):
var player_visible := is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked"))
var nearby_range := maxf(activation_range, weapon_range)
if player_visible and global_position.distance_to(_player.global_position) <= nearby_range:
return _player
return null
# Infected ships become fallback targets only when the player is absent nearby.
for ally in get_tree().get_nodes_in_group("virus_allies"):
if ally.is_queued_for_deletion() or float(ally.get("hull")) <= 0.0:
continue
var distance := global_position.distance_to(ally.global_position)
if distance <= nearby_range and distance < closest_distance:
closest = ally
closest_distance = distance
if closest != null:
return closest
return _player if player_visible else null
func apply_mine_hit() -> void:
if is_queued_for_deletion() or hull <= 0.0:
@ -341,7 +362,7 @@ func get_shot_damage() -> float:
func get_weapon_dps() -> float:
var interval := get_fire_interval()
return get_shot_damage() / maxf(0.001, interval) * (1.0 + 0.25 * minf(3.0, interval) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0)
return get_shot_damage() / maxf(0.001, interval) * (1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, interval) if weapon_kind == ItemDefinition.WeaponKind.GAMMA else 1.0)
func _reward_captain() -> void:
if is_instance_valid(_player):

View File

@ -1,5 +1,7 @@
extends Node
const DURATION := 10.0
var remaining := 0.0
var tick_damage := 0.0
var credited_to_player := false
@ -16,7 +18,7 @@ static func apply(target: Node, damage_per_second: float, player_hit: bool) -> v
status.set_script(load("res://scripts/game/radiation_status.gd"))
status.name = "RadiationStatus"
target.add_child(status)
status.remaining = 3.0
status.remaining = DURATION
status.tick_damage = maxf(status.tick_damage, damage_per_second)
status.credited_to_player = player_hit

View File

@ -199,6 +199,8 @@ func recycle_asteroid_fragment() -> void:
_update_shield_visual()
func try_control_enemy(enemy: CharacterBody2D) -> bool:
if not is_instance_valid(enemy) or int(enemy.get("loot_tier")) == 2:
return false
var limit := int(get_passive_extension_value("virus"))
if limit <= 0 or _is_destroyed:
return false
@ -208,6 +210,8 @@ func try_control_enemy(enemy: CharacterBody2D) -> bool:
controlled += 1
if controlled >= limit:
return false
if not bool(enemy.call("roll_virus_infection")):
return false
enemy.call("set_virus_controlled", true)
return true

View File

@ -24,7 +24,7 @@ func get_passive_description() -> String:
var value := get_passive_value()
match definition.passive_effect:
"recycler": return "Każdy zebrany odłamek asteroidy odnawia %.1f%% maksymalnych tarcz i energii." % value
"virus": return "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. Wrogowie nie wybierają ich jako celu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value)
"virus": return "Ma 50%% szans na przejęcie uciekającego wroga (poniżej 10%% kadłuba). Jedna próba na statek; losowanie następuje, gdy jest wolne miejsce na przejęcie. Zamiast uciekać atakują swoich dawnych sojuszników. Bossowie są odporni na wirusa. Wrogowie atakują zawirusowane statki, gdy gracza nie ma w pobliżu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value)
"titanium_armor": return "Pochłania 50%% otrzymywanych obrażeń, gdy kadłub ma nie więcej niż %d%% maksimum. Próg sprawdzany przed trafieniem." % int(value)
return ""

View File

@ -446,6 +446,6 @@ func get_total_dps() -> float:
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)
value *= 1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, item.definition.fire_interval)
total += value
return total

View File

@ -2,6 +2,8 @@ extends Control
signal next_level_requested
const BUTTON_STYLE := preload("res://scripts/ui/hud_button_style.gd")
const CREDIT_ICON := preload("res://assets/icons/resources/credits.svg")
@onready var next_button: Button = $Center/Panel/Contents/NextButton
@onready var buy_ammunition_button: Button = $Center/Panel/Contents/BuyAmmunitionButton
@onready var reset_skills_button: Button = $Center/Panel/Contents/ResetSkillsButton
@ -13,32 +15,30 @@ var _inventory: ShipInventory
var planet: Node2D
var _open_tween: Tween
var _missile_button: Button
var _gamma_buy: Button
var _stock_labels: Dictionary = {}
func _ready() -> void:
get_parent().get_node("FloatingWindows").call("register_window", self, panel, $Center/Panel/Contents/Title)
_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())
_missile_button = Button.new()
_missile_button.name = "BuyMissile"
var contents := $Center/Panel/Contents
contents.add_child(_gamma_buy)
contents.add_child(_missile_button)
reset_skills_button.hide()
_build_compact_shop()
get_parent().get_node("FloatingWindows").call("register_window", self, panel, contents.get_node("Title"))
next_button.pressed.connect(func() -> void: next_level_requested.emit())
buy_ammunition_button.pressed.connect(_buy_ammunition)
reset_skills_button.hide()
_missile_button = Button.new()
_missile_button.custom_minimum_size.y = 36
_missile_button.add_theme_font_size_override("font_size", 12)
_missile_button.focus_mode = Control.FOCUS_NONE
_missile_button.tooltip_text = "Jedna sztuka amunicji rakietowej do dowolnej wyrzutni."
_gamma_buy.pressed.connect(func() -> void:
if visible and _inventory:
_inventory.buy_irradiated_ammunition()
)
_missile_button.pressed.connect(func() -> void:
if visible and _inventory:
_inventory.buy_missile()
)
$Center/Panel/Contents.add_child(_missile_button)
$Center/Panel/Contents.move_child(_missile_button, reset_skills_button.get_index())
_inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory
if _inventory:
_inventory.ammunition_changed.connect(_refresh_ammunition_service)
@ -46,11 +46,100 @@ func _ready() -> void:
GameState.captain.changed.connect(_refresh_ammunition_service)
_refresh_ammunition_service()
func _build_compact_shop() -> void:
var contents := $Center/Panel/Contents
contents.add_theme_constant_override("separation", 7)
status_label.add_theme_font_size_override("font_size", 10)
status_label.add_theme_color_override("font_color", Color("86b7aa"))
services_label.add_theme_font_size_override("font_size", 11)
services_label.add_theme_color_override("font_color", Color("efc982"))
services_label.size_flags_vertical = Control.SIZE_FILL
var wallet := HBoxContainer.new()
wallet.name = "Wallet"
wallet.alignment = BoxContainer.ALIGNMENT_CENTER
wallet.add_theme_constant_override("separation", 6)
contents.add_child(wallet)
contents.move_child(wallet, 2)
var money_icon := TextureRect.new()
money_icon.texture = CREDIT_ICON
money_icon.custom_minimum_size = Vector2(15, 15)
money_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
money_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
money_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
wallet.add_child(money_icon)
services_label.reparent(wallet)
var offers := HBoxContainer.new()
offers.name = "Offers"
offers.add_theme_constant_override("separation", 6)
contents.add_child(offers)
contents.move_child(offers, 3)
_add_offer(offers, buy_ammunition_button, "ammunition", "Zwykła", 100, Color("e8b46f"))
_add_offer(offers, _gamma_buy, "irradiated_ammunition", "Radiacyjna", 10, Color("add580"))
_add_offer(offers, _missile_button, "missile", "Rakieta", 1, Color("82cddd"))
BUTTON_STYLE.apply(next_button, Color("78d7be"))
next_button.custom_minimum_size = Vector2(0, 34)
next_button.add_theme_font_size_override("font_size", 11)
next_button.icon = preload("res://assets/icons/extensions/teleport_art.png")
next_button.expand_icon = true
next_button.add_theme_constant_override("icon_max_width", 21)
contents.move_child(next_button, contents.get_child_count() - 1)
func _add_offer(offers: HBoxContainer, button: Button, item_id: String, caption: String, amount: int, accent: Color) -> void:
var card := PanelContainer.new()
card.name = item_id.capitalize().replace(" ", "") + "Offer"
card.custom_minimum_size.x = 94
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var style := StyleBoxFlat.new()
style.bg_color = Color("101e2a")
style.border_color = accent.darkened(0.65)
style.set_border_width_all(1)
style.border_width_top = 2
style.set_corner_radius_all(6)
style.set_content_margin_all(6)
card.add_theme_stylebox_override("panel", style)
offers.add_child(card)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 4)
card.add_child(column)
var icon := TextureRect.new()
icon.texture = load("res://assets/icons/resources/%s.png" % item_id)
icon.custom_minimum_size = Vector2(36, 36)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
column.add_child(icon)
var title := Label.new()
title.text = "%s ×%d" % [caption, amount]
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 10)
title.add_theme_color_override("font_color", accent.lightened(0.2))
column.add_child(title)
var stock := Label.new()
stock.name = "Stock"
stock.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
stock.add_theme_font_size_override("font_size", 9)
stock.add_theme_color_override("font_color", Color("8ba4b7"))
column.add_child(stock)
_stock_labels[item_id] = stock
button.reparent(column)
BUTTON_STYLE.apply(button, accent)
button.custom_minimum_size = Vector2(0, 28)
button.add_theme_font_size_override("font_size", 10)
button.icon = CREDIT_ICON
button.expand_icon = true
button.add_theme_constant_override("icon_max_width", 12)
for state in ["normal", "hover", "pressed", "disabled"]:
var frame := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
frame.set_content_margin_all(4)
frame.set_corner_radius_all(4)
frame.shadow_size = 0
button.add_theme_stylebox_override(state, frame)
func show_services(at_level_cap: bool, portal_open: bool = false) -> void:
_refresh_ammunition_service()
next_button.disabled = at_level_cap or portal_open
next_button.text = "OSIĄGNIĘTO OSTATNI UKŁAD" if at_level_cap else ("WORMHOLE OTWARTY" if portal_open else "IDŹ DO NASTĘPNEGO POZIOMU")
status_label.text = "Wleć w wormhole, aby przejść dalej." if portal_open else "Układ bezpieczny. Usługi dostępne."
next_button.text = "OSTATNI UKŁAD" if at_level_cap else ("WORMHOLE OTWARTY" if portal_open else "OTWÓRZ WORMHOLE")
status_label.text = "Wleć w wormhole, aby przejść dalej." if portal_open else "Bezpieczny układ · zaopatrzenie"
if visible:
return
show()
@ -81,30 +170,31 @@ func _position_at_planet() -> void:
size = panel.get_combined_minimum_size()
var viewport_size := get_viewport_rect().size
var sidebar := get_parent().get_node_or_null("ShipDrawer") as Control
var screen_right := viewport_size.x - (sidebar.size.x + 8.0 if sidebar != null and sidebar.visible else 8.0)
var screen_right := minf(viewport_size.x - 8.0, sidebar.get_global_rect().position.x - 8.0) if sidebar != null and sidebar.visible else viewport_size.x - 8.0
var target := planet_screen + Vector2(planet_radius + 24.0, -size.y * 0.5)
if target.x + size.x > screen_right:
target.x = planet_screen.x - planet_radius - 24.0 - size.x
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" % (_inventory.get_irradiated_ammunition_buy_cost() if _inventory else ShipInventory.IRRADIATED_AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level))
var ordinary_cost := _inventory.get_ammunition_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level))
var gamma_cost := _inventory.get_irradiated_ammunition_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.IRRADIATED_AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level))
var missile_cost := _inventory.get_missile_buy_cost() if _inventory else GameState.captain.discounted_price(ShipInventory.MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level))
buy_ammunition_button.text = "KUP %d" % ordinary_cost
_gamma_buy.text = "KUP %d" % gamma_cost
_missile_button.text = "KUP %d" % missile_cost
buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition()
_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()
reset_skills_button.text = "RESET SKILLI — %d KREDYTÓW" % GameState.captain.get_skill_reset_cost()
reset_skills_button.tooltip_text = "Zwraca wszystkie %d wydanych punktów nauki. Koszt: 100 kredytów × poziom kapitana." % GameState.captain.get_spent_learning_points()
buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition()
if _inventory:
buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()]
services_label.text = "Amunicja: %d • Kredyty: %d\nRekin biznesu: −%d%% ceny zakupów" % [_inventory.ammunition, GameState.captain.credits, roundi(GameState.captain.get_skill_bonus("business_shark") * 100)]
buy_ammunition_button.tooltip_text = "Kup 100 zwykłych pocisków za %d kredytów." % ordinary_cost
_gamma_buy.tooltip_text = "Kup 10 pocisków radiacyjnych za %d kredytów." % gamma_cost
_missile_button.tooltip_text = "Kup 1 rakietę za %d kredytów." % missile_cost
var discount := roundi(GameState.captain.get_skill_bonus("business_shark") * 100)
services_label.text = "%d kredytów%s" % [GameState.captain.credits, " · rabat %d%%" % discount if discount > 0 else ""]
_stock_labels["ammunition"].text = "Zapas: %d" % (_inventory.ammunition if _inventory else 0)
_stock_labels["irradiated_ammunition"].text = "Zapas: %d" % (_inventory.irradiated_ammunition if _inventory else 0)
_stock_labels["missile"].text = "Zapas: %d" % (_inventory.missile_ammunition if _inventory else 0)
func _buy_ammunition() -> void:
if visible and _inventory:
_inventory.buy_ammunition()
func _reset_skills() -> void:
if visible:
GameState.captain.reset_skills()

View File

@ -27,6 +27,14 @@ func enemy_at(point: Vector2) -> CharacterBody2D:
enemy.set("_weapon_timer", 100.0)
return enemy
func seed_successful_infection() -> void:
for value in 100:
seed(value)
if randf() < 0.5:
seed(value)
return
assert(false)
func _ready() -> void:
await get_tree().process_frame
GameState.start_new_run()
@ -108,6 +116,7 @@ func _ready() -> void:
var third := enemy_at(Vector2(450, 0))
first.hull = 1
second.hull = 1
seed_successful_infection()
first.call("_physics_process", 0.01)
second.call("_physics_process", 0.01)
assert(first.virus_controlled and not second.virus_controlled)
@ -135,6 +144,7 @@ func _ready() -> void:
assert(ship.shields == player_shields)
# Increased capacity takes another ship; downgrade and unequip release extras.
virus.passive_value = 2
seed_successful_infection()
second.call("_physics_process", 0.01)
assert(second.virus_controlled)
virus.passive_value = 1

View File

@ -0,0 +1,23 @@
extends Node
func _ready() -> void:
var script := preload("res://scripts/game/enemy_ship.gd")
seed(20261017)
var successes := 0
var failures := 0
for index in 2000:
var enemy := script.new()
var won: bool = enemy.roll_virus_infection()
successes += int(won)
failures += int(not won)
assert(not enemy.roll_virus_infection(), "A second frame must never reroll infection")
assert(not enemy.roll_virus_infection(), "Failed rolls must remain failed")
enemy.free()
assert(successes >= 900 and successes <= 1100)
assert(failures > 0 and successes > 0)
var boss := script.new()
boss.loot_tier = 2
for index in 100:
assert(not boss.roll_virus_infection())
boss.free()
print("VIRUS_CHANCE_TEST: PASS ", successes, "/2000 first attempts, no rerolls, bosses immune")
get_tree().quit()

View File

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

View File

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

View File

@ -25,6 +25,17 @@ func _ready() -> void:
infected.shields = infected.max_shields
assert(hostile.select_combat_target() == ship)
assert(infected.select_combat_target() == hostile)
ship.position = Vector2(-2000, 0)
assert(hostile.select_combat_target() == infected)
ship.position = Vector2.ZERO
assert(hostile.select_combat_target() == ship)
var boss := make_enemy(Vector2(4000, 0))
boss.apply_level_settings(1, 2)
boss.hull = 1.0
assert(not ship.try_control_enemy(boss))
boss.set_virus_controlled(true)
assert(not boss.virus_controlled and boss.is_in_group("enemies"))
boss.queue_free()
var xp_after_conversion := GameState.captain.experience
await get_tree().physics_frame
await get_tree().physics_frame

View File

@ -19,5 +19,21 @@ func _ready() -> void:
captain.tick_skills(0.11)
assert(not captain.is_skill_active("reactor_overload"))
assert(preload("res://scripts/captain/captain_skills.gd").description("reactor_overload", 1).contains("15 sekund"))
print("REACTOR_DURATION_TEST: PASS 15 seconds, 30-second cooldown, other skills still 5 seconds")
for rank in range(1, 4):
captain.skill_cooldowns.clear()
captain.skills["power_redirect"] = rank
assert(captain.equip_skill(2, "power_redirect"))
ship.shields = ship.max_shields
assert(captain.activate_skill("power_redirect", ship))
assert(ship.shields == 0.0)
assert(captain.skill_durations.power_redirect == 7.0)
assert(captain.skill_cooldowns.power_redirect == 30.0)
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [1.0, 1.8, 3.0][rank - 1]))
captain.tick_skills(6.99)
assert(captain.is_skill_active("power_redirect"))
captain.tick_skills(0.02)
assert(not captain.is_skill_active("power_redirect"))
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0))
assert(preload("res://scripts/captain/captain_skills.gd").description("power_redirect", 3).contains("7 sekund"))
print("REACTOR_DURATION_TEST: PASS overload 15s, redirection 7s at +100/180/300%, 30s cooldown, tactical maneuver 5s")
get_tree().quit()

View File

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

View File

@ -69,9 +69,9 @@ func _ready() -> void:
assert(is_equal_approx(enemy.hull, 76))
var status: Node = enemy.get_node("RadiationStatus")
status.set_physics_process(false)
for index in 6:
for index in 20:
status._physics_process(0.5)
assert(is_equal_approx(enemy.hull, 70))
assert(is_equal_approx(enemy.hull, 56))
assert(is_equal_approx(status.remaining, 0))
assert(enemy.get_node("Sprite2D").modulate == Color.WHITE)
for kind in 5:

View File

@ -0,0 +1,30 @@
extends Node
const RADIATION = preload("res://scripts/game/radiation_status.gd")
class Target:
extends CharacterBody2D
var total_damage := 0.0
func take_damage(amount: float, _critical: bool, _kind: int) -> void:
total_damage += amount
func take_weapon_hit(amount: float, critical: bool, kind: int) -> void:
take_damage(amount, critical, kind)
func _ready() -> void:
var target := Target.new()
add_child(target)
RADIATION.apply(target, 2.0, true)
var status := target.get_node("RadiationStatus")
status.set_physics_process(false)
assert(status.remaining == 10.0)
for index in 6:
status._physics_process(0.5)
assert(status.remaining == 7.0 and target.total_damage == 6.0)
RADIATION.apply(target, 1.0, true)
assert(status.remaining == 10.0 and status.tick_damage == 2.0)
for index in 20:
status._physics_process(0.5)
assert(status.remaining == 0.0 and target.total_damage == 26.0)
status._physics_process(1.0)
assert(target.total_damage == 26.0)
assert(preload("res://resources/items/gamma_cannon.tres").description.contains("10 s"))
print("RADIATION_DURATION_TEST: PASS 10s damage, refreshed timer, unchanged per-second damage, expiry and description")
get_tree().quit()

View File

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

View File

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

View File

@ -0,0 +1,44 @@
extends "res://scripts/game/game.gd"
func _ready() -> void:
super._ready()
set_physics_process(false)
player.set_physics_process(false)
player.get_node("WeaponController").set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.set_physics_process(false)
player.position = protected_planet.position + Vector2(float(protected_planet.get("radius")) + 110.0, 0)
player.reset_physics_interpolation()
GameState.captain.credits = 2500
GameState.captain.skills["business_shark"] = 2
var inventory := player.get_node("ShipInventory") as ShipInventory
inventory.ammunition = 240
inventory.irradiated_ammunition = 12
inventory.missile_ammunition = 7
services_menu.show_services(false)
await get_tree().create_timer(0.5).timeout
assert(services_menu.get("panel").size.x <= 340.0)
assert(services_menu.get("panel").size.y <= 290.0)
if "--capture-preview" in OS.get_cmdline_user_args():
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("res://artifacts/planet_services_preview.png")
print("PLANET_SERVICES_PREVIEW: captured ", services_menu.get("panel").size)
var credits := GameState.captain.credits
var cost := inventory.get_ammunition_buy_cost()
services_menu.get("buy_ammunition_button").pressed.emit()
assert(inventory.ammunition == 340 and GameState.captain.credits == credits - cost)
credits = GameState.captain.credits
cost = inventory.get_irradiated_ammunition_buy_cost()
services_menu.get("_gamma_buy").pressed.emit()
assert(inventory.irradiated_ammunition == 22 and GameState.captain.credits == credits - cost)
credits = GameState.captain.credits
cost = inventory.get_missile_buy_cost()
services_menu.get("_missile_button").pressed.emit()
assert(inventory.missile_ammunition == 8 and GameState.captain.credits == credits - cost)
GameState.captain.credits = 0
GameState.captain.emit_changed()
assert(services_menu.get("buy_ammunition_button").disabled and services_menu.get("_gamma_buy").disabled and services_menu.get("_missile_button").disabled)
services_menu.show_services(false, true)
assert(services_menu.get("next_button").disabled)
print("PLANET_SERVICES_TEST: PASS compact dimensions, all purchases, discounts, stock refresh, insufficient credits and portal state")
get_tree().quit()

View File

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

View File

@ -0,0 +1,5 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="PackedScene" path="res://scenes/game/game.tscn" id="1"]
[ext_resource type="Script" path="res://tests/previews/planet_services_preview.gd" id="2"]
[node name="PlanetServicesPreview" instance=ExtResource("1")]
script = ExtResource("2")