Zmiany
This commit is contained in:
parent
72839139eb
commit
458db974c0
BIN
assets/Ships/combat_drone.png
Normal file
BIN
assets/Ships/combat_drone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
40
assets/Ships/combat_drone.png.import
Normal file
40
assets/Ships/combat_drone.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d4hxo2htne3yy"
|
||||||
|
path="res://.godot/imported/combat_drone.png-1ee839fafe5ff3881843088267b833ba.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/Ships/combat_drone.png"
|
||||||
|
dest_files=["res://.godot/imported/combat_drone.png-1ee839fafe5ff3881843088267b833ba.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=true
|
||||||
|
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
|
||||||
BIN
assets/icons/captain/electrician.png
Normal file
BIN
assets/icons/captain/electrician.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
40
assets/icons/captain/electrician.png.import
Normal file
40
assets/icons/captain/electrician.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://baxcqpl7y3dfd"
|
||||||
|
path="res://.godot/imported/electrician.png-cd4ace91776ba47a19c9950998a779a9.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/captain/electrician.png"
|
||||||
|
dest_files=["res://.godot/imported/electrician.png-cd4ace91776ba47a19c9950998a779a9.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
|
||||||
BIN
assets/icons/extensions/combat_drones_art.png
Normal file
BIN
assets/icons/extensions/combat_drones_art.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
40
assets/icons/extensions/combat_drones_art.png.import
Normal file
40
assets/icons/extensions/combat_drones_art.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://huejfa7inar0"
|
||||||
|
path="res://.godot/imported/combat_drones_art.png-ce5df9b0b0abb663da961f963a875f37.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/extensions/combat_drones_art.png"
|
||||||
|
dest_files=["res://.godot/imported/combat_drones_art.png-ce5df9b0b0abb663da961f963a875f37.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
|
||||||
17
assets/icons/new_skills_art.md
Normal file
17
assets/icons/new_skills_art.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Elektryk i drony bojowe
|
||||||
|
|
||||||
|
Wygenerowano narzędziem wbudowanym `image_gen`, z przezroczystym tłem.
|
||||||
|
|
||||||
|
## Elektryk — captain/electrician.png
|
||||||
|
|
||||||
|
Use case: stylized-concept. Asset type: square transparent sci-fi spaceship game skill icon, high quality painted game UI art. Subject: Elektryk, futuristic engineer's armored gloved hand holding a compact metallic powerbank with a brilliant cyan lightning core, copper wire contacts and tiny electric arcs. Single strong readable centered silhouette, navy steel copper and electric turquoise palette, crisp bevels, cinematic rim lighting, detailed yet readable at 48px, generous transparent margins. No text, letters, frame, background or watermark. Save the PNG asset for the project.
|
||||||
|
|
||||||
|
## Drony bojowe — extensions/combat_drones_art.png
|
||||||
|
|
||||||
|
Use case: stylized-concept. Asset type: transparent square sci-fi game equipment icon. Subject three compact autonomous combat drones in triangular formation, metallic gunmetal hulls with angular swept wings, twin small kinetic gun barrels and bright cyan engine cores. Top down three quarter view, polished hand painted realistic spaceship inventory art, crisp strong silhouettes readable at 48px, restrained gold accents and cyan rim lighting, isolated centered with transparent margins. No lettering, frame, background or watermark. Save PNG for game project.
|
||||||
|
|
||||||
|
## Dron w rozgrywce — ../Ships/combat_drone.png
|
||||||
|
|
||||||
|
Wygenerowano wbudowanym `image_gen`, z przezroczystym tłem. Kadłub jest teksturą; silniki i błyski działek animuje combat_drone.gd.
|
||||||
|
|
||||||
|
Use case: stylized-concept. Asset type: production gameplay sprite for a top-down 2D space shooter, transparent PNG. ONE small autonomous combat drone spaceship, perfectly orthographic TOP DOWN view, nose pointing straight UP, bilateral symmetry. Compact gunmetal titanium armored fuselage, swept angular short wings, two small forward-facing mechanical autocannons ending near the nose, two cyan engine exhaust ports at rear, tiny turquoise sensors, restrained brass details. Detailed painted realistic sci-fi spacecraft, crisp edges and clear silhouette readable at 44 pixels tall. Centered entire ship with 12 percent transparent padding. Lighting from upper left, dark panel grooves and bright metallic edge highlights. No pilot, perspective, isometric angle, cast shadow, engine flame, muzzle flash, text, UI, frame, scenery, stars, watermark or other spacecraft. Engines and muzzle flashes are animated separately in the game.
|
||||||
@ -7,7 +7,7 @@ script = ExtResource("2")
|
|||||||
ability_id = "cloak"
|
ability_id = "cloak"
|
||||||
cooldown = 60.0
|
cooldown = 60.0
|
||||||
display_name = "Kamuflaż"
|
display_name = "Kamuflaż"
|
||||||
description = "Przez 10 s statek staje się niemal niewidoczny. Przeciwnicy nie wykrywają go ani nie śledzą. Nie zapewnia odporności na już wystrzelone pociski. Odnowienie: 60 s."
|
description = "Przez 10 s statek staje się niemal niewidoczny. Przeciwnicy nie wykrywają go ani nie śledzą. Oddanie strzału przerywa kamuflaż. Nie zapewnia odporności na już wystrzelone pociski. Odnowienie: 60 s."
|
||||||
kind = 1
|
kind = 1
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|||||||
19
resources/items/combat_drones.tres
Normal file
19
resources/items/combat_drones.tres
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
|
||||||
|
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
|
||||||
|
[ext_resource type="Texture2D" path="res://assets/icons/extensions/combat_drones_art.png" id="icon"]
|
||||||
|
[sub_resource type="Resource" id="Ability"]
|
||||||
|
script = ExtResource("2")
|
||||||
|
ability_id = "combat_drones"
|
||||||
|
cooldown = 30.0
|
||||||
|
display_name = "Drony bojowe"
|
||||||
|
description = "Wypuszcza 3 drony na 10 s. Strzelają działkami mechanicznymi; łącznie zadają 50% DPS statku w momencie startu. Koszt: 50 szt. zwykłej amunicji. Odnowienie: 30 s."
|
||||||
|
kind = 5
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
icon = ExtResource("icon")
|
||||||
|
item_id = "combat_drones"
|
||||||
|
display_name = "Drony bojowe"
|
||||||
|
category = 5
|
||||||
|
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
|
||||||
|
active_ability = SubResource("Ability")
|
||||||
@ -1,9 +1,16 @@
|
|||||||
extends ActiveAbility
|
extends ActiveAbility
|
||||||
|
|
||||||
@export_enum("Teleport", "Cloak", "Mine", "Flares", "Solar shields") var kind := 0
|
@export_enum("Teleport", "Cloak", "Mine", "Flares", "Solar shields", "Combat drones") var kind := 0
|
||||||
|
|
||||||
func unavailable_reason(ship: CharacterBody2D) -> String:
|
func unavailable_reason(ship: CharacterBody2D) -> String:
|
||||||
match kind:
|
match kind:
|
||||||
|
5:
|
||||||
|
if float(ship.get_node("ExtensionSystems").get("drones_remaining")) > 0.0:
|
||||||
|
return "Drony już walczą"
|
||||||
|
if int(ship.get_node("ShipInventory").get("ammunition")) < 50:
|
||||||
|
return "Wymagane 50 szt. zwykłej amunicji"
|
||||||
|
if float(ship.get_node("ShipInventory").call("get_total_dps")) <= 0.0:
|
||||||
|
return "Wymagana zamontowana broń"
|
||||||
0:
|
0:
|
||||||
if float(ship.get("energy")) <= 0.0:
|
if float(ship.get("energy")) <= 0.0:
|
||||||
return "Brak energii"
|
return "Brak energii"
|
||||||
|
|||||||
@ -32,6 +32,9 @@ func get_skill_rank(skill: String) -> int:
|
|||||||
func get_skill_bonus(skill: String) -> float:
|
func get_skill_bonus(skill: String) -> float:
|
||||||
return SKILLS.value(skill, get_skill_rank(skill))
|
return SKILLS.value(skill, get_skill_rank(skill))
|
||||||
|
|
||||||
|
func get_energy_bank_fraction() -> float:
|
||||||
|
return maxf(0.10, get_skill_bonus("electrician"))
|
||||||
|
|
||||||
func normalize_skill_ranks() -> void:
|
func normalize_skill_ranks() -> void:
|
||||||
for skill in skills.keys():
|
for skill in skills.keys():
|
||||||
if not SKILLS.DATA.has(skill):
|
if not SKILLS.DATA.has(skill):
|
||||||
|
|||||||
@ -3,12 +3,13 @@ extends RefCounted
|
|||||||
const MAX_RANK := 3
|
const MAX_RANK := 3
|
||||||
const COOLDOWN := 30.0
|
const COOLDOWN := 30.0
|
||||||
const DURATION := 5.0
|
const DURATION := 5.0
|
||||||
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer"]
|
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician"]
|
||||||
const DATA := {
|
const DATA := {
|
||||||
|
"electrician": {"name": "Elektryk", "active": false, "values": [0.15, 0.20, 0.25], "color": "70f5df"},
|
||||||
"lucky": {"name": "Farciarz", "active": false, "values": [0.05, 0.10, 0.25], "color": "e8c76f"},
|
"lucky": {"name": "Farciarz", "active": false, "values": [0.05, 0.10, 0.25], "color": "e8c76f"},
|
||||||
"explorer": {"name": "Eksplorator", "active": false, "values": [1, 3, 6], "color": "76d9cc"},
|
"explorer": {"name": "Eksplorator", "active": false, "values": [1, 3, 6], "color": "76d9cc"},
|
||||||
"business_shark": {"name": "Rekin biznesu", "active": false, "values": [0.10, 0.30, 0.60], "color": "efb568"},
|
"business_shark": {"name": "Rekin biznesu", "active": false, "values": [0.10, 0.30, 0.60], "color": "efb568"},
|
||||||
"reactor_overload": {"name": "Przeciążenie reaktora", "active": true, "values": [0.10, 0.20, 0.70], "color": "63d9f5"},
|
"reactor_overload": {"name": "Przeciążenie reaktora", "active": true, "values": [0.20, 0.50, 1.00], "color": "63d9f5"},
|
||||||
"tactical_maneuver": {"name": "Manewr taktyczny", "active": true, "values": [0.30, 0.60, 1.00], "color": "b9a0ef"},
|
"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"},
|
"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"},
|
"miner": {"name": "Górnik", "active": false, "values": [3, 6, 10], "color": "e5ac76"},
|
||||||
@ -27,6 +28,8 @@ static func icon(skill: String) -> Texture2D:
|
|||||||
static func description(skill: String, rank: int) -> String:
|
static func description(skill: String, rank: int) -> String:
|
||||||
var amount := roundi(value(skill, rank) * 100.0)
|
var amount := roundi(value(skill, rank) * 100.0)
|
||||||
match skill:
|
match skill:
|
||||||
|
"electrician":
|
||||||
|
return "Powerbank przywraca %d%% maksymalnej energii zamiast 10%%. Umiejętność pasywna; premie rang nie sumują się." % amount
|
||||||
"lucky":
|
"lucky":
|
||||||
return "Szczęście: %d%% szans na przedmiot o 1 poziom wyższy niż bazowy loot planszy. Dotyczy lootu z wrogów i znalezisk. Bazowo: 0%%. Sumuje się ze sprzętem do limitu 60%%. Premie rang nie sumują się." % amount
|
return "Szczęście: %d%% szans na przedmiot o 1 poziom wyższy niż bazowy loot planszy. Dotyczy lootu z wrogów i znalezisk. Bazowo: 0%%. Sumuje się ze sprzętem do limitu 60%%. Premie rang nie sumują się." % amount
|
||||||
"explorer":
|
"explorer":
|
||||||
|
|||||||
116
scripts/game/combat_drone.gd
Normal file
116
scripts/game/combat_drone.gd
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn")
|
||||||
|
const FIRE_INTERVAL := 0.25
|
||||||
|
const HULL_TEXTURE := preload("res://assets/Ships/combat_drone.png")
|
||||||
|
var mother: CharacterBody2D
|
||||||
|
var formation_index := 0
|
||||||
|
var damage_per_shot := 1.0
|
||||||
|
var remaining := 10.0
|
||||||
|
var _fire_timer := 0.0
|
||||||
|
var _elapsed := 0.0
|
||||||
|
var _flash := 0.0
|
||||||
|
var velocity := Vector2.ZERO
|
||||||
|
var _turn_velocity := 0.0
|
||||||
|
const FORMATION := [Vector2(0, -110), Vector2(-35, -78), Vector2(35, -78)]
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("player_combat_drones")
|
||||||
|
z_index = 4
|
||||||
|
scale = Vector2.ONE * 0.5
|
||||||
|
rotation = mother.rotation
|
||||||
|
velocity = mother.velocity
|
||||||
|
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||||
|
_fire_timer = formation_index * FIRE_INTERVAL / 3.0
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
remaining -= delta
|
||||||
|
if remaining <= 0.0 or not is_instance_valid(mother) or bool(mother.get("_is_destroyed")) or bool(mother.get("is_docked")):
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
_elapsed += delta
|
||||||
|
_flash = maxf(0.0, _flash - delta)
|
||||||
|
_fire_timer = maxf(0.0, _fire_timer - delta)
|
||||||
|
var target := _find_target()
|
||||||
|
var destination := formation_position()
|
||||||
|
var escort_velocity := mother.velocity
|
||||||
|
if target != null:
|
||||||
|
var anchor := mother.global_position.lerp(target.global_position, 0.45)
|
||||||
|
destination = anchor + Vector2.from_angle(TAU * formation_index / 3.0 + _elapsed * 0.7) * 85.0
|
||||||
|
escort_velocity *= 0.55
|
||||||
|
else:
|
||||||
|
# Rotating formation slots have tangential velocity as the mother turns.
|
||||||
|
var offset := destination - mother.global_position
|
||||||
|
escort_velocity += Vector2(-offset.y, offset.x) * float(mother.get("angular_velocity"))
|
||||||
|
var error := destination - global_position
|
||||||
|
var correction := (error * 3.0).limit_length(260.0)
|
||||||
|
var desired_velocity := (escort_velocity + correction).limit_length(maxf(360.0, mother.velocity.length() + 200.0))
|
||||||
|
velocity = velocity.move_toward(desired_velocity, maxf(650.0, mother.velocity.length()) * delta)
|
||||||
|
global_position += velocity * delta
|
||||||
|
var desired_rotation := mother.rotation
|
||||||
|
if target != null:
|
||||||
|
var aim := target.global_position - global_position
|
||||||
|
desired_rotation = aim.angle() + PI * 0.5
|
||||||
|
if _fire_timer <= 0.0 and aim.length() <= 650.0 and _clear_shot(target):
|
||||||
|
_fire(target)
|
||||||
|
_fire_timer = FIRE_INTERVAL
|
||||||
|
elif error.length() > 55.0 and velocity.length() > 30.0:
|
||||||
|
desired_rotation = velocity.angle() + PI * 0.5
|
||||||
|
var angle_error := angle_difference(rotation, desired_rotation)
|
||||||
|
_turn_velocity = move_toward(_turn_velocity, clampf(angle_error * 6.0, -4.5, 4.5), 14.0 * delta)
|
||||||
|
var turn := _turn_velocity * delta
|
||||||
|
if absf(turn) >= absf(angle_error) and signf(turn) == signf(angle_error):
|
||||||
|
rotation = desired_rotation
|
||||||
|
_turn_velocity = 0.0
|
||||||
|
else:
|
||||||
|
rotation += turn
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func formation_position() -> Vector2:
|
||||||
|
return mother.global_position + FORMATION[clampi(formation_index, 0, 2)].rotated(mother.rotation)
|
||||||
|
|
||||||
|
func _find_target() -> Node2D:
|
||||||
|
var closest: Node2D
|
||||||
|
var distance := 800.0
|
||||||
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
|
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
|
||||||
|
continue
|
||||||
|
var candidate := global_position.distance_to(enemy.global_position)
|
||||||
|
if candidate < distance and mother.global_position.distance_to(enemy.global_position) < 1000.0:
|
||||||
|
distance = candidate
|
||||||
|
closest = enemy
|
||||||
|
return closest
|
||||||
|
|
||||||
|
func _clear_shot(target: Node2D) -> bool:
|
||||||
|
var query := PhysicsRayQueryParameters2D.create(global_position, target.global_position, 13)
|
||||||
|
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
||||||
|
return hit.is_empty() or hit.get("collider") == target
|
||||||
|
|
||||||
|
func _fire(target: Node2D) -> void:
|
||||||
|
var direction := (target.global_position - global_position).normalized()
|
||||||
|
var bullet := PROJECTILE.instantiate()
|
||||||
|
bullet.call("configure", global_position + direction * 9.0, direction, ItemDefinition.WeaponKind.MACHINE_GUN, 1100.0, damage_per_shot, 650.0)
|
||||||
|
get_parent().add_child(bullet)
|
||||||
|
_flash = 0.06
|
||||||
|
mother.get_node("ExtensionSystems").call("break_cloak_on_fire")
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
modulate.a = minf(clampf(_elapsed / 0.2, 0.0, 1.0), clampf(remaining / 0.6, 0.0, 1.0))
|
||||||
|
var flicker := sin(_elapsed * 33.0 + formation_index * 2.1)
|
||||||
|
for x in [-5.5, 5.5]:
|
||||||
|
var exhaust := Vector2(x, 19.5)
|
||||||
|
draw_circle(exhaust + Vector2(0, 5), 11.0 + flicker, Color(0.08, 0.65, 1.0, 0.08))
|
||||||
|
draw_circle(exhaust + Vector2(0, 3), 6.0, Color(0.12, 0.8, 1.0, 0.18))
|
||||||
|
draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-3, 0), exhaust + Vector2(0, 15 + flicker * 2), exhaust + Vector2(3, 0)]), Color(0.15, 0.72, 1.0, 0.65))
|
||||||
|
draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-1.4, 0), exhaust + Vector2(0, 8 + flicker), exhaust + Vector2(1.4, 0)]), Color("c8fbff"))
|
||||||
|
var hull_size := HULL_TEXTURE.get_size() * (60.0 / HULL_TEXTURE.get_height())
|
||||||
|
var hull_rect := Rect2(-hull_size * 0.5, hull_size)
|
||||||
|
draw_texture_rect(HULL_TEXTURE, Rect2(hull_rect.position + Vector2(3, 5), hull_size), false, Color(0, 0, 0, 0.4))
|
||||||
|
draw_texture_rect(HULL_TEXTURE, hull_rect, false)
|
||||||
|
if _flash > 0.0:
|
||||||
|
for x in [-5.5, 5.5]:
|
||||||
|
var muzzle := Vector2(x, -20)
|
||||||
|
draw_circle(muzzle, 8.0, Color(1.0, 0.6, 0.18, 0.15))
|
||||||
|
draw_colored_polygon(PackedVector2Array([muzzle + Vector2(-3, 0), muzzle + Vector2(-1.5, -4), muzzle + Vector2(0, -12), muzzle + Vector2(1.5, -4), muzzle + Vector2(3, 0)]), Color("ffbd68"))
|
||||||
|
draw_line(muzzle, muzzle + Vector2(0, -7), Color("fff2cb"), 2.0, true)
|
||||||
1
scripts/game/combat_drone.gd.uid
Normal file
1
scripts/game/combat_drone.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://f2rww506fhi7
|
||||||
@ -21,6 +21,10 @@ const LASER_MISS_CHANCE := 0.35
|
|||||||
@export var weapon_range := 680.0
|
@export var weapon_range := 680.0
|
||||||
@export var weapon_cooldown := 1.5
|
@export var weapon_cooldown := 1.5
|
||||||
@export var weapon_damage := 8.0
|
@export var weapon_damage := 8.0
|
||||||
|
enum Tactic { RAM, KEEP_DISTANCE }
|
||||||
|
@export_enum("Losowa", "Taran", "Dystans") var tactic_choice := 0
|
||||||
|
var tactic := Tactic.RAM
|
||||||
|
var _orbit_direction := 1.0
|
||||||
|
|
||||||
var max_shields := 20.0
|
var max_shields := 20.0
|
||||||
var shields := 20.0
|
var shields := 20.0
|
||||||
@ -37,6 +41,8 @@ var _slow_remaining := 0.0
|
|||||||
var _combat_target: Node2D
|
var _combat_target: Node2D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
tactic = randi_range(0, 1) if tactic_choice == 0 else tactic_choice - 1
|
||||||
|
_orbit_direction = -1.0 if randf() < 0.5 else 1.0
|
||||||
hull = max_hull
|
hull = max_hull
|
||||||
_sprite = $Sprite2D
|
_sprite = $Sprite2D
|
||||||
_shield_visual = Node2D.new()
|
_shield_visual = Node2D.new()
|
||||||
@ -108,10 +114,12 @@ func _physics_process(delta: float) -> void:
|
|||||||
_activated = true
|
_activated = true
|
||||||
if _activated and to_player.length_squared() > 1.0:
|
if _activated and to_player.length_squared() > 1.0:
|
||||||
var direction := to_player.normalized()
|
var direction := to_player.normalized()
|
||||||
var desired_rotation := direction.angle() + PI * 0.5
|
var fleeing := is_retreating()
|
||||||
|
var movement := combat_movement(to_player)
|
||||||
|
var desired_rotation := (movement if fleeing else direction).angle() + PI * 0.5
|
||||||
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
|
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
|
||||||
velocity = velocity.move_toward(direction * approach_speed * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta)
|
velocity = velocity.move_toward(movement * approach_speed * (1.35 if fleeing else 1.0) * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta)
|
||||||
if to_player.length() <= weapon_range and _weapon_timer <= 0.0:
|
if not fleeing and to_player.length() <= weapon_range and _weapon_timer <= 0.0:
|
||||||
_fire_plasma(direction)
|
_fire_plasma(direction)
|
||||||
_weapon_timer = get_fire_interval()
|
_weapon_timer = get_fire_interval()
|
||||||
else:
|
else:
|
||||||
@ -120,6 +128,29 @@ func _physics_process(delta: float) -> void:
|
|||||||
move_and_slide()
|
move_and_slide()
|
||||||
_check_player_collision()
|
_check_player_collision()
|
||||||
|
|
||||||
|
func is_retreating() -> bool:
|
||||||
|
return hull < max_hull * 0.5
|
||||||
|
|
||||||
|
func combat_movement(to_target: Vector2) -> Vector2:
|
||||||
|
var distance := to_target.length()
|
||||||
|
var direction := to_target.normalized()
|
||||||
|
if is_retreating():
|
||||||
|
var escape := -direction
|
||||||
|
var half := preload("res://scripts/levels/system_generator.gd").WORLD_SIZE * 0.5 - Vector2(120, 120)
|
||||||
|
if absf(global_position.x) > half.x:
|
||||||
|
escape.x = -signf(global_position.x)
|
||||||
|
if absf(global_position.y) > half.y:
|
||||||
|
escape.y = -signf(global_position.y)
|
||||||
|
return escape.normalized()
|
||||||
|
if tactic == Tactic.RAM:
|
||||||
|
return direction
|
||||||
|
var preferred := minf(weapon_range * 0.65, 420.0)
|
||||||
|
if distance < preferred * 0.8:
|
||||||
|
return -direction
|
||||||
|
if distance > preferred * 1.15:
|
||||||
|
return direction
|
||||||
|
return direction.orthogonal() * _orbit_direction * 0.55
|
||||||
|
|
||||||
func get_slow_multiplier() -> float:
|
func get_slow_multiplier() -> float:
|
||||||
return 0.5 if _slow_remaining > 0.0 else 1.0
|
return 0.5 if _slow_remaining > 0.0 else 1.0
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ const PULSE := preload("res://scripts/game/extension_pulse.gd")
|
|||||||
var cooldowns: Dictionary = {}
|
var cooldowns: Dictionary = {}
|
||||||
var cloak_remaining := 0.0
|
var cloak_remaining := 0.0
|
||||||
var solar_remaining := 0.0
|
var solar_remaining := 0.0
|
||||||
|
var drones_remaining := 0.0
|
||||||
|
var _drones: Array[Node2D] = []
|
||||||
var _cloak_material: ShaderMaterial
|
var _cloak_material: ShaderMaterial
|
||||||
var _old_material: Material
|
var _old_material: Material
|
||||||
var _shadow_was_visible := false
|
var _shadow_was_visible := false
|
||||||
@ -15,11 +17,16 @@ var _has_destination := false
|
|||||||
|
|
||||||
func reset_combat_state() -> void:
|
func reset_combat_state() -> void:
|
||||||
cooldowns.clear()
|
cooldowns.clear()
|
||||||
|
_clear_drones()
|
||||||
solar_remaining = 0.0
|
solar_remaining = 0.0
|
||||||
if cloak_remaining > 0.0:
|
if cloak_remaining > 0.0:
|
||||||
_end_cloak()
|
_end_cloak()
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
if drones_remaining > 0.0:
|
||||||
|
drones_remaining = maxf(0.0, drones_remaining - delta)
|
||||||
|
if drones_remaining <= 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")):
|
||||||
|
_clear_drones()
|
||||||
for id in cooldowns.keys():
|
for id in cooldowns.keys():
|
||||||
cooldowns[id] = maxf(0.0, float(cooldowns[id]) - delta)
|
cooldowns[id] = maxf(0.0, float(cooldowns[id]) - delta)
|
||||||
if cloak_remaining > 0.0:
|
if cloak_remaining > 0.0:
|
||||||
@ -66,6 +73,25 @@ func has_teleport_destination() -> bool:
|
|||||||
|
|
||||||
func activate(kind: int) -> void:
|
func activate(kind: int) -> void:
|
||||||
match kind:
|
match kind:
|
||||||
|
5:
|
||||||
|
var inventory := ship.get_node("ShipInventory") as ShipInventory
|
||||||
|
if inventory.ammunition < 50 or drones_remaining > 0.0:
|
||||||
|
return
|
||||||
|
inventory.ammunition -= 50
|
||||||
|
inventory.ammunition_changed.emit()
|
||||||
|
drones_remaining = 10.0
|
||||||
|
break_cloak_on_fire()
|
||||||
|
var shared_dps := inventory.get_total_dps() * 0.5
|
||||||
|
for index in 3:
|
||||||
|
var drone := Node2D.new()
|
||||||
|
drone.set_script(preload("res://scripts/game/combat_drone.gd"))
|
||||||
|
drone.set("mother", ship)
|
||||||
|
drone.set("formation_index", index)
|
||||||
|
drone.set("damage_per_shot", shared_dps / 3.0 * 0.25)
|
||||||
|
drone.position = ship.position + Vector2.from_angle(TAU * index / 3.0) * 48.0
|
||||||
|
ship.get_parent().add_child(drone)
|
||||||
|
_drones.append(drone)
|
||||||
|
_deployment_effect(ship.position, Vector2.DOWN.rotated(ship.rotation), Color("70f5df"))
|
||||||
0:
|
0:
|
||||||
if not has_teleport_destination():
|
if not has_teleport_destination():
|
||||||
return
|
return
|
||||||
@ -103,6 +129,16 @@ func activate(kind: int) -> void:
|
|||||||
solar_remaining = 5.0
|
solar_remaining = 5.0
|
||||||
_pulse(ship.global_position, Color("70dfff"), 50.0)
|
_pulse(ship.global_position, Color("70dfff"), 50.0)
|
||||||
|
|
||||||
|
func _clear_drones() -> void:
|
||||||
|
drones_remaining = 0.0
|
||||||
|
for drone in _drones:
|
||||||
|
if is_instance_valid(drone):
|
||||||
|
drone.queue_free()
|
||||||
|
_drones.clear()
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_clear_drones()
|
||||||
|
|
||||||
func _teleport_effect(point: Vector2, arriving: bool) -> void:
|
func _teleport_effect(point: Vector2, arriving: bool) -> void:
|
||||||
var effect := Node2D.new()
|
var effect := Node2D.new()
|
||||||
effect.set_script(preload("res://scripts/game/teleport_vortex.gd"))
|
effect.set_script(preload("res://scripts/game/teleport_vortex.gd"))
|
||||||
@ -138,6 +174,10 @@ func _start_cloak() -> void:
|
|||||||
visual.modulate = Color(visual.modulate, 0.08)
|
visual.modulate = Color(visual.modulate, 0.08)
|
||||||
_pulse(ship.global_position, Color("81e4ef"), 40.0)
|
_pulse(ship.global_position, Color("81e4ef"), 40.0)
|
||||||
|
|
||||||
|
func break_cloak_on_fire() -> void:
|
||||||
|
if cloak_remaining > 0.0:
|
||||||
|
_end_cloak()
|
||||||
|
|
||||||
func _end_cloak() -> void:
|
func _end_cloak() -> void:
|
||||||
cloak_remaining = 0.0
|
cloak_remaining = 0.0
|
||||||
ship.get_node("Sprite2D").set("material", _old_material)
|
ship.get_node("Sprite2D").set("material", _old_material)
|
||||||
|
|||||||
@ -178,6 +178,14 @@ func _create_sidebar() -> void:
|
|||||||
preload("res://scripts/ui/hud_button_style.gd").apply(_debug_item_button, Color("a9ef83"))
|
preload("res://scripts/ui/hud_button_style.gd").apply(_debug_item_button, Color("a9ef83"))
|
||||||
_debug_item_button.pressed.connect(_give_debug_item)
|
_debug_item_button.pressed.connect(_give_debug_item)
|
||||||
_debug_actions.add_child(_debug_item_button)
|
_debug_actions.add_child(_debug_item_button)
|
||||||
|
var ammunition_button := Button.new()
|
||||||
|
ammunition_button.name = "AddAmmunition"
|
||||||
|
ammunition_button.text = "Dodaj amunicję"
|
||||||
|
ammunition_button.tooltip_text = "+10 zwykłej amunicji, +10 radiacyjnej amunicji i +10 rakiet"
|
||||||
|
ammunition_button.custom_minimum_size.y = 36
|
||||||
|
preload("res://scripts/ui/hud_button_style.gd").apply(ammunition_button, Color("efb568"))
|
||||||
|
ammunition_button.pressed.connect(_give_debug_ammunition)
|
||||||
|
_debug_actions.add_child(ammunition_button)
|
||||||
_debug_item_status = Label.new()
|
_debug_item_status = Label.new()
|
||||||
_debug_item_status.add_theme_font_size_override("font_size", 10)
|
_debug_item_status.add_theme_font_size_override("font_size", 10)
|
||||||
_debug_item_status.add_theme_color_override("font_color", Color("a9ef83"))
|
_debug_item_status.add_theme_color_override("font_color", Color("a9ef83"))
|
||||||
@ -348,7 +356,7 @@ func _update_energy_bank_button() -> void:
|
|||||||
var inventory := player.get_node("ShipInventory") as ShipInventory
|
var inventory := player.get_node("ShipInventory") as ShipInventory
|
||||||
var count := inventory.get_energy_bank_count()
|
var count := inventory.get_energy_bank_count()
|
||||||
(_energy_bank_button.get_node("Amount") as Label).text = str(count)
|
(_energy_bank_button.get_node("Amount") as Label).text = str(count)
|
||||||
_energy_bank_button.tooltip_text = "Bank energii: %d\nR • Przywraca 10%% maksymalnej energii" % count
|
_energy_bank_button.tooltip_text = "Bank energii: %d\nR • Przywraca %d%% maksymalnej energii" % [count, roundi(GameState.captain.get_energy_bank_fraction() * 100.0)]
|
||||||
_energy_bank_button.disabled = count == 0 or float(player.get("energy")) >= float(player.get("max_energy")) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked"))
|
_energy_bank_button.disabled = count == 0 or float(player.get("energy")) >= float(player.get("max_energy")) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked"))
|
||||||
_energy_bank_button.call("set_visual_state", 0.0, false, _energy_bank_button.disabled)
|
_energy_bank_button.call("set_visual_state", 0.0, false, _energy_bank_button.disabled)
|
||||||
|
|
||||||
@ -389,6 +397,14 @@ func _toggle_debug() -> void:
|
|||||||
_update_debug_toggle()
|
_update_debug_toggle()
|
||||||
_layout_debug()
|
_layout_debug()
|
||||||
|
|
||||||
|
func _give_debug_ammunition() -> void:
|
||||||
|
var inventory := player.get_node("ShipInventory") as ShipInventory
|
||||||
|
inventory.ammunition += 10
|
||||||
|
inventory.irradiated_ammunition += 10
|
||||||
|
inventory.missile_ammunition += 10
|
||||||
|
inventory.ammunition_changed.emit()
|
||||||
|
inventory.inventory_changed.emit()
|
||||||
|
|
||||||
func _give_debug_item() -> bool:
|
func _give_debug_item() -> bool:
|
||||||
var inventory := player.get_node("ShipInventory") as ShipInventory
|
var inventory := player.get_node("ShipInventory") as ShipInventory
|
||||||
_debug_item_status.show()
|
_debug_item_status.show()
|
||||||
|
|||||||
@ -436,7 +436,7 @@ func activate_ability(slot_index: int) -> bool:
|
|||||||
func use_energy_bank() -> void:
|
func use_energy_bank() -> void:
|
||||||
if $ShipInventory.use_energy_bank():
|
if $ShipInventory.use_energy_bank():
|
||||||
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
|
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
|
||||||
popup.text = "+10% ENERGII"
|
popup.text = "+%d%% ENERGII" % roundi(GameState.captain.get_energy_bank_fraction() * 100.0)
|
||||||
popup.set("text_color", Color("70f5df"))
|
popup.set("text_color", Color("70f5df"))
|
||||||
popup.position = position + Vector2(-48, -60)
|
popup.position = position + Vector2(-48, -60)
|
||||||
get_parent().add_child(popup)
|
get_parent().add_child(popup)
|
||||||
|
|||||||
@ -135,6 +135,7 @@ func try_fire_missile(slot_index: int = 2) -> bool:
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
func _fire_projectile(item: ItemInstance) -> void:
|
func _fire_projectile(item: ItemInstance) -> void:
|
||||||
|
_break_cloak_on_fire()
|
||||||
var definition := item.definition
|
var definition := item.definition
|
||||||
var hit: Dictionary = ship.call("roll_weapon_hit", item)
|
var hit: Dictionary = ship.call("roll_weapon_hit", item)
|
||||||
var muzzle_position := _muzzle_position()
|
var muzzle_position := _muzzle_position()
|
||||||
@ -194,6 +195,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
|
|||||||
line.visible = false
|
line.visible = false
|
||||||
return
|
return
|
||||||
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, ItemDefinition.WeaponKind.LASER)
|
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, ItemDefinition.WeaponKind.LASER)
|
||||||
|
_break_cloak_on_fire()
|
||||||
if not result.is_empty():
|
if not result.is_empty():
|
||||||
var hit: Dictionary = ship.call("roll_weapon_hit", item)
|
var hit: Dictionary = ship.call("roll_weapon_hit", item)
|
||||||
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]))
|
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]))
|
||||||
@ -201,6 +203,11 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
|
|||||||
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
|
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
|
||||||
_shot_timers[slot_index] = definition.fire_interval
|
_shot_timers[slot_index] = definition.fire_interval
|
||||||
|
|
||||||
|
func _break_cloak_on_fire() -> void:
|
||||||
|
var systems := ship.get_node_or_null("ExtensionSystems")
|
||||||
|
if systems != null:
|
||||||
|
systems.call("break_cloak_on_fire")
|
||||||
|
|
||||||
func _aim_direction_from(origin: Vector2) -> Vector2:
|
func _aim_direction_from(origin: Vector2) -> Vector2:
|
||||||
var offset := ship.get_global_mouse_position() - origin
|
var offset := ship.get_global_mouse_position() - origin
|
||||||
return offset.normalized() if offset.length_squared() > 0.001 else Vector2.UP.rotated(ship.rotation)
|
return offset.normalized() if offset.length_squared() > 0.001 else Vector2.UP.rotated(ship.rotation)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const DEFINITIONS := [
|
const DEFINITIONS := [
|
||||||
|
preload("res://resources/items/combat_drones.tres"),
|
||||||
preload("res://resources/items/teleport.tres"),
|
preload("res://resources/items/teleport.tres"),
|
||||||
preload("res://resources/items/cloak.tres"),
|
preload("res://resources/items/cloak.tres"),
|
||||||
preload("res://resources/items/mine_layer.tres"),
|
preload("res://resources/items/mine_layer.tres"),
|
||||||
|
|||||||
@ -93,7 +93,7 @@ func use_energy_bank() -> bool:
|
|||||||
var current := float(ship.get("energy"))
|
var current := float(ship.get("energy"))
|
||||||
if current >= maximum:
|
if current >= maximum:
|
||||||
return false
|
return false
|
||||||
ship.set("energy", minf(maximum, current + maximum * 0.1))
|
ship.set("energy", minf(maximum, current + maximum * GameState.captain.get_energy_bank_fraction()))
|
||||||
energy_banks -= 1
|
energy_banks -= 1
|
||||||
inventory_changed.emit()
|
inventory_changed.emit()
|
||||||
return true
|
return true
|
||||||
|
|||||||
111
tests/abilities/electrician_drones_tactics_test.gd
Normal file
111
tests/abilities/electrician_drones_tactics_test.gd
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const PLAYER := preload("res://scenes/game/player_ship.tscn")
|
||||||
|
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
|
||||||
|
const DRONES := preload("res://resources/items/combat_drones.tres")
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.start_new_run()
|
||||||
|
var ship := PLAYER.instantiate() as CharacterBody2D
|
||||||
|
add_child(ship)
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
var weapons := ship.get_node("WeaponController")
|
||||||
|
weapons.set_physics_process(false)
|
||||||
|
var systems := ship.get_node("ExtensionSystems")
|
||||||
|
systems.set_physics_process(false)
|
||||||
|
var inventory := ship.get_node("ShipInventory") as ShipInventory
|
||||||
|
# Every rank replaces the baseline, respects the energy cap, and uses one bank.
|
||||||
|
for rank in range(4):
|
||||||
|
GameState.captain.skills["electrician"] = rank
|
||||||
|
inventory.energy_banks = 2
|
||||||
|
ship.set("energy", 0.0)
|
||||||
|
assert(inventory.use_energy_bank())
|
||||||
|
assert(is_equal_approx(float(ship.get("energy")), float(ship.get("max_energy")) * [0.10, 0.15, 0.20, 0.25][rank]))
|
||||||
|
ship.set("energy", float(ship.get("max_energy")) - 1.0)
|
||||||
|
assert(inventory.use_energy_bank())
|
||||||
|
assert(ship.get("energy") == ship.get("max_energy"))
|
||||||
|
assert(inventory.energy_banks == 0)
|
||||||
|
assert(preload("res://scripts/captain/captain_skills.gd").icon("electrician") != null)
|
||||||
|
assert(not GameState.captain.equip_skill(0, "electrician"))
|
||||||
|
# Tactics move toward, away from, or around their target, then flee below 50%.
|
||||||
|
var enemy := ENEMY.instantiate() as CharacterBody2D
|
||||||
|
enemy.position = Vector2(250, 0)
|
||||||
|
add_child(enemy)
|
||||||
|
enemy.set_physics_process(false)
|
||||||
|
enemy.set("_player", ship)
|
||||||
|
enemy.set("tactic", 0)
|
||||||
|
assert(enemy.call("combat_movement", Vector2(200, 0)).x > 0.0)
|
||||||
|
enemy.set("tactic", 1)
|
||||||
|
assert(enemy.call("combat_movement", Vector2(100, 0)).x < 0.0)
|
||||||
|
assert(enemy.call("combat_movement", Vector2(700, 0)).x > 0.0)
|
||||||
|
assert(absf(enemy.call("combat_movement", Vector2(420, 0)).y) > 0.0)
|
||||||
|
enemy.set("hull", float(enemy.get("max_hull")) * 0.5)
|
||||||
|
assert(not enemy.call("is_retreating"))
|
||||||
|
enemy.set("hull", float(enemy.get("max_hull")) * 0.49)
|
||||||
|
for tactic in [0, 1]:
|
||||||
|
enemy.set("tactic", tactic)
|
||||||
|
assert(enemy.call("is_retreating"))
|
||||||
|
assert(enemy.call("combat_movement", Vector2(500, 0)).x < 0.0)
|
||||||
|
enemy.set("hull", 10000.0)
|
||||||
|
# Module is lootable, charges exactly 50 ammo even with the ammunition passive.
|
||||||
|
assert(DRONES in preload("res://scripts/items/item_generator.gd").DEFINITIONS)
|
||||||
|
var module := ItemInstance.new()
|
||||||
|
module.definition = DRONES
|
||||||
|
inventory.extension_slots[0] = module
|
||||||
|
GameState.captain.skills["efficient_magazine"] = 3
|
||||||
|
inventory.ammunition = 49
|
||||||
|
assert(not ship.call("activate_ability", 0))
|
||||||
|
assert(inventory.ammunition == 49)
|
||||||
|
inventory.ammunition = 100
|
||||||
|
var dps := inventory.get_total_dps()
|
||||||
|
assert(dps > 0.0)
|
||||||
|
assert(ship.call("activate_ability", 0))
|
||||||
|
assert(inventory.ammunition == 50)
|
||||||
|
assert(not ship.call("activate_ability", 0))
|
||||||
|
var drones := get_tree().get_nodes_in_group("player_combat_drones")
|
||||||
|
assert(drones.size() == 3)
|
||||||
|
var total_dps := 0.0
|
||||||
|
for drone in drones:
|
||||||
|
drone.set_physics_process(false)
|
||||||
|
total_dps += float(drone.get("damage_per_shot")) / float(drone.FIRE_INTERVAL)
|
||||||
|
assert(is_equal_approx(total_dps, dps * 0.5))
|
||||||
|
assert(is_equal_approx(inventory.get_total_dps(), dps))
|
||||||
|
await get_tree().physics_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
var before := float(enemy.get("hull")) + float(enemy.get("shields"))
|
||||||
|
drones[0].position = Vector2(100, 0)
|
||||||
|
drones[0].call("_fire", enemy)
|
||||||
|
for bullet in get_children():
|
||||||
|
if bullet is Area2D and bullet.get_script() == preload("res://scripts/game/weapon_projectile.gd"):
|
||||||
|
bullet.call("_physics_process", 0.2)
|
||||||
|
assert(float(enemy.get("hull")) + float(enemy.get("shields")) < before, "Drone bullets must damage enemies")
|
||||||
|
assert(inventory.ammunition == 50, "No extra ammunition charged per shot")
|
||||||
|
systems.call("_physics_process", 10.0)
|
||||||
|
for drone in drones:
|
||||||
|
assert(drone.is_queued_for_deletion())
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
|
||||||
|
systems.call("reset_combat_state")
|
||||||
|
assert(ship.call("activate_ability", 0))
|
||||||
|
var deployed := get_tree().get_nodes_in_group("player_combat_drones")
|
||||||
|
for drone in deployed:
|
||||||
|
drone.set_physics_process(false)
|
||||||
|
get_tree().paused = true
|
||||||
|
assert(not ship.call("activate_ability", 0))
|
||||||
|
await get_tree().create_timer(0.05, true).timeout
|
||||||
|
assert(float(systems.get("drones_remaining")) == 10.0)
|
||||||
|
get_tree().paused = false
|
||||||
|
ship.set("is_docked", true)
|
||||||
|
systems.call("_physics_process", 0.1)
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
|
||||||
|
ship.set("is_docked", false)
|
||||||
|
systems.call("reset_combat_state")
|
||||||
|
inventory.ammunition = 50
|
||||||
|
assert(ship.call("activate_ability", 0))
|
||||||
|
systems.call("reset_combat_state")
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
|
||||||
|
assert(float(systems.get("drones_remaining")) == 0.0)
|
||||||
|
print("ELECTRICIAN_DRONES_TACTICS_TEST: PASS (four bank strengths/caps, passive icon, tactics and retreat, loot module, exact ammo cost, 3 drones, shared 50% DPS, actual bullet damage, lifetime and reset)")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/abilities/electrician_drones_tactics_test.gd.uid
Normal file
1
tests/abilities/electrician_drones_tactics_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://c1xp2w857yvwq
|
||||||
4
tests/abilities/electrician_drones_tactics_test.tscn
Normal file
4
tests/abilities/electrician_drones_tactics_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/abilities/electrician_drones_tactics_test.gd" id="1"]
|
||||||
|
[node name="ElectricianDronesTacticsTest" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
@ -80,6 +80,30 @@ func _ready() -> void:
|
|||||||
assert(enemy.call("select_combat_target") == ship)
|
assert(enemy.call("select_combat_target") == ship)
|
||||||
assert(not ship.call("activate_ability", 1))
|
assert(not ship.call("activate_ability", 1))
|
||||||
# Mine: 5% maximum hull damage despite armor; slow lasts five seconds; one hit only.
|
# Mine: 5% maximum hull damage despite armor; slow lasts five seconds; one hit only.
|
||||||
|
var weapons := ship.get_node("WeaponController")
|
||||||
|
for weapon_id in ["plasma_cannon", "machine_gun", "gamma_cannon", "missile_launcher", "beam_laser"]:
|
||||||
|
systems.get("cooldowns").clear()
|
||||||
|
assert(ship.call("activate_ability", 1))
|
||||||
|
var weapon := GENERATOR.generate_for_definition(load("res://resources/items/%s.tres" % weapon_id), 1)
|
||||||
|
if weapon_id == "beam_laser":
|
||||||
|
ship.set("energy", 0.0)
|
||||||
|
weapons.call("_fire_laser", 0, weapon)
|
||||||
|
assert(ship.call("is_cloaked"), "Failed shot must preserve cloak")
|
||||||
|
ship.set("energy", ship.get("max_energy"))
|
||||||
|
weapons.call("_fire_laser", 0, weapon)
|
||||||
|
elif weapon_id == "missile_launcher":
|
||||||
|
inventory.weapon_slots[2] = weapon
|
||||||
|
inventory.missile_ammunition = 0
|
||||||
|
assert(not weapons.call("try_fire_missile", 2))
|
||||||
|
assert(ship.call("is_cloaked"), "Empty launcher must preserve cloak")
|
||||||
|
inventory.missile_ammunition = 1
|
||||||
|
assert(weapons.call("try_fire_missile", 2))
|
||||||
|
else:
|
||||||
|
weapons.call("_fire_projectile", weapon)
|
||||||
|
assert(not ship.call("is_cloaked"), "Every fired weapon must break cloak")
|
||||||
|
assert(ship.get_node("Sprite2D").material == original_material)
|
||||||
|
assert(ship.call("get_ability_cooldown", 1) == 60.0)
|
||||||
|
assert(enemy.call("select_combat_target") == ship)
|
||||||
assert(ship.call("activate_ability", 2))
|
assert(ship.call("activate_ability", 2))
|
||||||
var mine: Node2D = get_tree().get_nodes_in_group("player_mines")[0]
|
var mine: Node2D = get_tree().get_nodes_in_group("player_mines")[0]
|
||||||
mine.set_physics_process(false)
|
mine.set_physics_process(false)
|
||||||
|
|||||||
@ -44,10 +44,10 @@ func _ready() -> void:
|
|||||||
captain.skill_durations.clear()
|
captain.skill_durations.clear()
|
||||||
var base_regeneration: float = ship.get_energy_regeneration()
|
var base_regeneration: float = ship.get_energy_regeneration()
|
||||||
assert(captain.activate_skill("reactor_overload", ship))
|
assert(captain.activate_skill("reactor_overload", ship))
|
||||||
assert(is_equal_approx(ship.get_energy_regeneration(), base_regeneration * [1.1, 1.2, 1.7][rank - 1]))
|
assert(is_equal_approx(ship.get_energy_regeneration(), base_regeneration * [1.2, 1.5, 2.0][rank - 1]))
|
||||||
ship.energy = 0.0
|
ship.energy = 0.0
|
||||||
ship._physics_process(0.1)
|
ship._physics_process(0.1)
|
||||||
assert(is_equal_approx(ship.energy, base_regeneration * [1.1, 1.2, 1.7][rank - 1] * 0.1))
|
assert(is_equal_approx(ship.energy, base_regeneration * [1.2, 1.5, 2.0][rank - 1] * 0.1))
|
||||||
assert(not captain.activate_skill("reactor_overload", ship))
|
assert(not captain.activate_skill("reactor_overload", ship))
|
||||||
assert(captain.activate_skill("tactical_maneuver", ship))
|
assert(captain.activate_skill("tactical_maneuver", ship))
|
||||||
assert(is_equal_approx(ship.get_evasion_chance(), [0.3, 0.6, 1.0][rank - 1]))
|
assert(is_equal_approx(ship.get_evasion_chance(), [0.3, 0.6, 1.0][rank - 1]))
|
||||||
@ -61,7 +61,7 @@ func _ready() -> void:
|
|||||||
ship.shields = ship.max_shields * 0.25
|
ship.shields = ship.max_shields * 0.25
|
||||||
assert(captain.activate_skill("power_redirect", ship))
|
assert(captain.activate_skill("power_redirect", ship))
|
||||||
assert(ship.shields == 0.0)
|
assert(ship.shields == 0.0)
|
||||||
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [0.1, 0.2, 0.7][rank - 1] + [0.5, 0.9, 1.5][rank - 1]))
|
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [0.2, 0.5, 1.0][rank - 1] + [0.5, 0.9, 1.5][rank - 1]))
|
||||||
ship.shield_on_damage_percent = 100.0
|
ship.shield_on_damage_percent = 100.0
|
||||||
ship.restore_shields_from_damage(100.0)
|
ship.restore_shields_from_damage(100.0)
|
||||||
assert(ship.shields == 0.0)
|
assert(ship.shields == 0.0)
|
||||||
|
|||||||
63
tests/ships/debug_ammo_drone_formation_test.gd
Normal file
63
tests/ships/debug_ammo_drone_formation_test.gd
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.start_new_run()
|
||||||
|
var game := preload("res://scenes/game/game.tscn").instantiate()
|
||||||
|
add_child(game)
|
||||||
|
game.set_physics_process(false)
|
||||||
|
var ship := game.get_node("PlayerShip") as CharacterBody2D
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
|
var systems := ship.get_node("ExtensionSystems")
|
||||||
|
systems.set_physics_process(false)
|
||||||
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
|
enemy.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
var hud := game.get_node("HUD")
|
||||||
|
hud.call("_toggle_debug")
|
||||||
|
var inventory := ship.get_node("ShipInventory") as ShipInventory
|
||||||
|
var before := Vector3i(inventory.ammunition, inventory.irradiated_ammunition, inventory.missile_ammunition)
|
||||||
|
var button := hud.get_node("HUDRoot/DebugActions/AddAmmunition") as Button
|
||||||
|
button.pressed.emit()
|
||||||
|
button.pressed.emit()
|
||||||
|
assert(Vector3i(inventory.ammunition, inventory.irradiated_ammunition, inventory.missile_ammunition) == before + Vector3i(20, 20, 20))
|
||||||
|
systems.call("activate", 5)
|
||||||
|
var drones := get_tree().get_nodes_in_group("player_combat_drones")
|
||||||
|
assert(drones.size() == 3)
|
||||||
|
for drone in drones:
|
||||||
|
drone.set_physics_process(false)
|
||||||
|
assert(drone.scale == Vector2(0.5, 0.5))
|
||||||
|
# Stationary formation converges to fixed positions in front, with no idle orbit.
|
||||||
|
_step(ship, drones, 120)
|
||||||
|
for drone in drones:
|
||||||
|
assert(drone.global_position.distance_to(drone.call("formation_position")) < 2.0)
|
||||||
|
assert((drone.global_position - ship.global_position).dot(Vector2.UP.rotated(ship.rotation)) > 60.0)
|
||||||
|
# Forward flight and a turn inherit mother velocity without position snapping.
|
||||||
|
ship.velocity = Vector2(0, -200)
|
||||||
|
var previous_velocity: Vector2 = drones[0].get("velocity")
|
||||||
|
_step(ship, drones, 1)
|
||||||
|
assert((drones[0].get("velocity") as Vector2).distance_to(previous_velocity) <= 650.0 / 60.0 + 0.01)
|
||||||
|
_step(ship, drones, 119)
|
||||||
|
for drone in drones:
|
||||||
|
assert(drone.global_position.distance_to(drone.call("formation_position")) < 12.0)
|
||||||
|
ship.set("angular_velocity", 0.6)
|
||||||
|
_step(ship, drones, 120, true)
|
||||||
|
for drone in drones:
|
||||||
|
assert(drone.global_position.distance_to(drone.call("formation_position")) < 15.0)
|
||||||
|
ship.set("angular_velocity", 0.0)
|
||||||
|
ship.velocity = Vector2.ZERO
|
||||||
|
_step(ship, drones, 120)
|
||||||
|
for drone in drones:
|
||||||
|
assert(drone.global_position.distance_to(drone.call("formation_position")) < 2.0)
|
||||||
|
assert((drone.get("velocity") as Vector2).length() < 3.0)
|
||||||
|
print("DEBUG_AMMO_DRONE_FORMATION_TEST: PASS (debug ammo x10 repeatable, half scale, fixed forward formation, acceleration bound, moving/turning mother tracking, smooth stop)")
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
func _step(ship: CharacterBody2D, drones: Array[Node], frames: int, turning := false) -> void:
|
||||||
|
for frame in frames:
|
||||||
|
if turning:
|
||||||
|
ship.rotation += 0.6 / 60.0
|
||||||
|
ship.velocity = Vector2.UP.rotated(ship.rotation) * 200.0
|
||||||
|
ship.position += ship.velocity / 60.0
|
||||||
|
for drone in drones:
|
||||||
|
drone.call("_physics_process", 1.0 / 60.0)
|
||||||
1
tests/ships/debug_ammo_drone_formation_test.gd.uid
Normal file
1
tests/ships/debug_ammo_drone_formation_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://b0bk1yhyw8t0y
|
||||||
4
tests/ships/debug_ammo_drone_formation_test.tscn
Normal file
4
tests/ships/debug_ammo_drone_formation_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/ships/debug_ammo_drone_formation_test.gd" id="1"]
|
||||||
|
[node name="DebugAmmoDroneFormationTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
Loading…
x
Reference in New Issue
Block a user