diff --git a/resources/items/scanner.tres b/resources/items/scanner.tres new file mode 100644 index 0000000..df01663 --- /dev/null +++ b/resources/items/scanner.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=4 format=3] +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"] +[ext_resource type="Script" path="res://scripts/abilities/scanner_ability.gd" id="2"] +[sub_resource type="Resource" id="Ability"] +script = ExtResource("2") +display_name = "Skaner" +description = "Przez 8 sekund pokazuje kierunki do wrogów, asteroid, wraków i planet z antycznymi znaleziskami. Wymaga i zużywa 50% maksymalnej energii." +[resource] +script = ExtResource("1") +item_id = "scanner" +display_name = "Skaner" +category = 5 +description = "Aktywne rozszerzenie eksploracyjne. Zamontuj w slocie modułu i aktywuj przypisanym klawiszem 1–6." +active_ability = SubResource("Ability") diff --git a/scripts/abilities/scanner_ability.gd b/scripts/abilities/scanner_ability.gd new file mode 100644 index 0000000..f68dcb0 --- /dev/null +++ b/scripts/abilities/scanner_ability.gd @@ -0,0 +1,24 @@ +extends ActiveAbility + +const SCAN_DURATION := 8.0 +const OVERLAY := preload("res://scripts/ui/scanner_overlay.gd") + +func unavailable_reason(ship: CharacterBody2D) -> String: + if ship.has_node("ScannerOverlay"): + return "Skaner jest aktywny" + return "Wymagane 50% energii" if float(ship.get("energy")) < float(ship.get("max_energy")) * 0.5 else "" + +func activate(ship: CharacterBody2D) -> void: + if not unavailable_reason(ship).is_empty(): + return + ship.set("energy", float(ship.get("energy")) - float(ship.get("max_energy")) * 0.5) + var layer := CanvasLayer.new() + layer.name = "ScannerOverlay" + layer.layer = 8 + ship.add_child(layer) + var overlay := Control.new() + overlay.set_script(OVERLAY) + overlay.set("player", ship) + overlay.set("remaining", SCAN_DURATION) + layer.add_child(overlay) + overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) diff --git a/scripts/abilities/scanner_ability.gd.uid b/scripts/abilities/scanner_ability.gd.uid new file mode 100644 index 0000000..7ec96ce --- /dev/null +++ b/scripts/abilities/scanner_ability.gd.uid @@ -0,0 +1 @@ +uid://dhayld6xbljao diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index 5c4deb7..b985275 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -14,6 +14,7 @@ const DEFINITIONS := [ preload("res://resources/items/afterburner.tres"), preload("res://resources/items/shield_charge.tres"), preload("res://resources/items/repair_drones.tres"), + preload("res://resources/items/scanner.tres"), ] const AFFIX_DATABASE := preload("res://resources/items/affix_database.tres") diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 90dd1ce..490dc1d 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -8,7 +8,7 @@ const CARGO_SIZE := 20 const WEAPON_SLOT_COUNT := 3 const MISSILE_SLOT := 2 const STARTING_MISSILE_AMMUNITION := 2 -const MISSILE_CRAFT_PARTS_COST := 20.0 +const MISSILE_CRAFT_PARTS_COST := 50.0 const MISSILE_BUY_BASE_COST := 10 const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres")] const MODULE_SLOT_COUNT := 6 @@ -215,7 +215,7 @@ func _recalculate_ship_stats() -> void: for item in weapon_slots + module_slots: if item == null or item.definition == null: continue - for stat_key in ["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", "shield_on_damage_percent"]: + for stat_key in ["max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]: bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key) ship.call("apply_equipment_bonuses", bonuses) diff --git a/scripts/ui/scanner_overlay.gd b/scripts/ui/scanner_overlay.gd new file mode 100644 index 0000000..6eb6f3c --- /dev/null +++ b/scripts/ui/scanner_overlay.gd @@ -0,0 +1,58 @@ +extends Control + +const FONT := preload("res://assets/fonts/font2.ttf") +var player: CharacterBody2D +var remaining := 8.0 +var targets: Array[Node2D] = [] + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + for group in ["enemies", "exploration_pois"]: + for target in get_tree().get_nodes_in_group(group): + if player.get_parent().is_ancestor_of(target): + targets.append(target) + +func _process(delta: float) -> void: + remaining -= delta + if remaining <= 0 or not is_instance_valid(player) or bool(player.get("_is_destroyed")): + get_parent().queue_free() + return + queue_redraw() + +func _draw() -> void: + if not is_instance_valid(player): + return + var bounds := Rect2(Vector2(45, 100), Vector2(maxf(100, size.x - 390), maxf(100, size.y - 210))) + var origin := get_viewport().get_canvas_transform() * player.global_position + var anchor := Vector2(clampf(origin.x, bounds.position.x, bounds.end.x), clampf(origin.y, bounds.position.y, bounds.end.y)) + for target in targets: + if not is_instance_valid(target) or target.is_queued_for_deletion(): + continue + var color := Color("ff7777") + var caption := "WRÓG" + if target.is_in_group("enemies"): + if float(target.get("hull")) <= 0: + continue + else: + match str(target.get("kind")): + "asteroid": + color = Color("a6c8e0") + caption = "AST" + "wreck": + color = Color("ffd166") + caption = "WRAK" + "ruin": + color = Color("d2a4ff") + caption = "?" + var screen := get_viewport().get_canvas_transform() * target.global_position + var direction := (screen - anchor).normalized() + var point := screen + if not bounds.has_point(screen): + var dx := (bounds.end.x - anchor.x) if direction.x > 0 else (anchor.x - bounds.position.x) + var dy := (bounds.end.y - anchor.y) if direction.y > 0 else (anchor.y - bounds.position.y) + var distance := minf(dx / maxf(absf(direction.x), 0.001), dy / maxf(absf(direction.y), 0.001)) + point = anchor + direction * distance + var side := Vector2(-direction.y, direction.x) + draw_colored_polygon(PackedVector2Array([point + direction * 10, point - direction * 6 + side * 6, point - direction * 6 - side * 6]), color) + draw_string(FONT, point + Vector2(-18, 25), caption, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, color) + draw_string(FONT, Vector2(32, 82), "SKANER %.1f s · czerwony: wrogowie · błękitny: asteroidy · złoty: wraki · fioletowy: antyczne planety" % maxf(0, remaining), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color("c9e8ff")) diff --git a/scripts/ui/scanner_overlay.gd.uid b/scripts/ui/scanner_overlay.gd.uid new file mode 100644 index 0000000..5da2bc8 --- /dev/null +++ b/scripts/ui/scanner_overlay.gd.uid @@ -0,0 +1 @@ +uid://bx7eud4hxp4jh diff --git a/tests/abilities/scanner_test.gd b/tests/abilities/scanner_test.gd new file mode 100644 index 0000000..bd2881e --- /dev/null +++ b/tests/abilities/scanner_test.gd @@ -0,0 +1,46 @@ +extends Node2D +const GENERATOR := preload("res://scripts/items/item_generator.gd") +func _ready() -> void: + GameState.start_new_run() + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() as CharacterBody2D + ship.name = "PlayerShip" + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var inventory := ship.get_node("ShipInventory") as ShipInventory + var scanner := GENERATOR.generate_for_definition(preload("res://resources/items/scanner.tres"), 5) + assert(scanner.get_addon_count() > 0) + assert(inventory.add_item(scanner)) + assert(inventory.move_item("cargo", inventory.cargo.find(scanner), "module", 0)) + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() as CharacterBody2D + enemy.position = Vector2(900, 0) + add_child(enemy) + enemy.set_physics_process(false) + for kind in ["asteroid", "wreck", "ruin"]: + var poi := StaticBody2D.new() + poi.set_script(preload("res://scripts/levels/exploration_poi.gd")) + poi.set("kind", kind) + poi.set("player", ship) + poi.position = Vector2(1500, 100) + add_child(poi) + poi.set_physics_process(false) + var maximum := float(ship.get("max_energy")) + ship.set("energy", maximum * 0.49) + assert(not ship.activate_ability(0)) + ship.set("energy", maximum * 0.8) + assert(ship.activate_ability(0)) + assert(is_equal_approx(ship.get("energy"), maximum * 0.3)) + var layer := ship.get_node("ScannerOverlay") as CanvasLayer + var overlay := layer.get_child(0) as Control + assert(overlay.get("targets").size() == 4) + assert(overlay.mouse_filter == Control.MOUSE_FILTER_IGNORE) + assert(not ship.activate_ability(0)) + await get_tree().process_frame + overlay.call("_process", 8.1) + await get_tree().process_frame + assert(not ship.has_node("ScannerOverlay")) + ship.set("energy", maximum * 0.5) + assert(ship.activate_ability(0)) + assert(is_zero_approx(ship.get("energy"))) + print("SCANNER_TEST: PASS loot addons, module activation, 50% max energy cost, four target types, repeat prevention, 8s expiry, reactivation") + get_tree().quit() diff --git a/tests/abilities/scanner_test.gd.uid b/tests/abilities/scanner_test.gd.uid new file mode 100644 index 0000000..0381b38 --- /dev/null +++ b/tests/abilities/scanner_test.gd.uid @@ -0,0 +1 @@ +uid://c1a6tu0q5xiut diff --git a/tests/abilities/scanner_test.tscn b/tests/abilities/scanner_test.tscn new file mode 100644 index 0000000..2dadf15 --- /dev/null +++ b/tests/abilities/scanner_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/abilities/scanner_test.gd" id="1"] +[node name="ScannerTest" type="Node2D"] +script = ExtResource("1")