extends Node2D const PLAYER := preload("res://scenes/game/player_ship.tscn") const ENEMY := preload("res://scenes/game/enemy_ship.tscn") const GENERATOR := preload("res://scripts/items/item_generator.gd") const ROLLS := preload("res://scripts/items/passive_extension_rolls.gd") const ORE := preload("res://scripts/items/mineral_pickup.gd") var ship: CharacterBody2D var inventory: ShipInventory func equip(id: String, slot: int, value: float) -> ItemInstance: var item := GENERATOR.generate_for_definition(load("res://resources/items/%s.tres" % id), 1) assert(item.is_passive_extension() and item.definition.icon != null) item.passive_value = value assert(inventory.add_item(item)) assert(inventory.move_item("cargo", inventory.cargo.find(item), "extension", slot)) assert(not ship.activate_ability(slot)) assert(item.get_tooltip_text().contains("PASYWNE")) return item func enemy_at(point: Vector2) -> CharacterBody2D: var enemy := ENEMY.instantiate() as CharacterBody2D enemy.position = point add_child(enemy) enemy.set("_player", ship) enemy.set_physics_process(false) enemy.set("_weapon_timer", 100.0) return enemy func _ready() -> void: await get_tree().process_frame GameState.start_new_run() ship = PLAYER.instantiate() ship.name = "PlayerShip" add_child(ship) ship.set_physics_process(false) ship.get_node("WeaponController").set_physics_process(false) ship.get_node("ExtensionSystems").set_physics_process(false) inventory = ship.get_node("ShipInventory") var recycler := equip("recycler", 0, 0.5) var virus := equip("virus", 1, 1) var armor := equip("titanium_armor", 2, 5) # Per-item values are stable across resource serialization. var encoded := var_to_bytes_with_objects(recycler) var restored: ItemInstance = bytes_to_var_with_objects(encoded) assert(restored.get_passive_value() == 0.5) # Every discrete value is reachable, with strict exponential tails. for effect in ["recycler", "virus", "titanium_armor"]: var steps: int = {"recycler": 79, "virus": 19, "titanium_armor": 45}[effect] var base: float = {"recycler": 0.5, "virus": 1.0, "titanium_armor": 5.0}[effect] var step := 0.5 if effect == "recycler" else 1.0 assert(is_equal_approx(ROLLS.roll_value(effect, 0.0), base + steps * step)) assert(is_equal_approx(ROLLS.roll_value(effect, 0.99999), base)) for rank in range(steps + 1): var high := ROLLS.tail_chance(float(rank) / steps) var low := ROLLS.tail_chance(float(rank + 1) / steps) if rank < steps else 0.0 assert(is_equal_approx(ROLLS.roll_value(effect, (high + low) * 0.5), base + rank * step)) assert(is_equal_approx(ROLLS.tail_chance(1.0), 0.0002)) # Actual ore pickup both credits one mineral and restores resources once. ship.energy = 0 ship.shields = 0 var before := int(GameState.captain.minerals.get("iron", 0)) var ore := Node2D.new() ore.set_script(ORE) add_child(ore) ore.set_physics_process(false) ore.call("_physics_process", 1.0) assert(is_equal_approx(ship.energy, ship.max_energy * 0.005)) assert(is_equal_approx(ship.shields, ship.max_shields * 0.005)) assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1) ore.call("_physics_process", 1.0) assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1) # Best duplicate wins; restoration is capped, not additive. recycler.passive_value = 40 equip("recycler", 3, 10) ship.energy = ship.max_energy * 0.9 ship.shields = ship.max_shields * 0.9 ship.recycle_asteroid_fragment() assert(ship.energy == ship.max_energy and ship.shields == ship.max_shields) assert(ship.get_passive_extension_value("recycler") == 40) # Armor reduces incoming damage (including shields) only at its threshold. ship.hull = ship.max_hull * 0.05 ship.shields = 50 ship.take_damage(20) assert(is_equal_approx(ship.shields, 40)) ship.hull = ship.max_hull * 0.051 ship.shields = 50 ship.take_damage(20) assert(is_equal_approx(ship.shields, 30)) armor.passive_value = 50 ship.hull = ship.max_hull * 0.5 assert(is_equal_approx(ship.get_damage_reduction(), 0.5)) # Virus takes fleeing ships only, respects capacity and switches allegiance. var first := enemy_at(Vector2(200, 0)) var second := enemy_at(Vector2(320, 0)) var third := enemy_at(Vector2(450, 0)) first.hull = 1 second.hull = 1 first.call("_physics_process", 0.01) second.call("_physics_process", 0.01) assert(first.virus_controlled and not second.virus_controlled) assert(not first.is_in_group("enemies") and first.is_in_group("virus_allies")) assert(not first.is_retreating() and first.select_combat_target() != ship) assert(second.select_combat_target() == first) await get_tree().physics_frame assert(first.collision_layer == 64) # Controlled fire uses enemy damage, cannot hit the player and can hit hostiles. first.weapon_kind = ItemDefinition.WeaponKind.MACHINE_GUN first.call("_fire_plasma", Vector2.RIGHT) var bullet: Node for child in get_children(): if child is Area2D and child.get_script() == preload("res://scripts/game/weapon_projectile.gd"): bullet = child assert(bullet != null and bullet.collision_mask == 37 and bullet.enemy_shot) var shields_before: float = third.shields bullet.call("_apply_damage_once", third) assert(third.shields < shields_before) # Converted missile splash must never damage the player. var player_shields: float = ship.shields bullet.weapon_kind = ItemDefinition.WeaponKind.MISSILE bullet.global_position = ship.global_position bullet.call("_explode_at_target", third) assert(ship.shields == player_shields) # Increased capacity takes another ship; downgrade and unequip release extras. virus.passive_value = 2 second.call("_physics_process", 0.01) assert(second.virus_controlled) virus.passive_value = 1 ship.call("_sync_virus_allies") assert(get_tree().get_nodes_in_group("virus_allies").size() == 1) assert(inventory.move_item("extension", 1, "cargo", 2)) ship.call("_sync_virus_allies") assert(get_tree().get_nodes_in_group("virus_allies").is_empty()) assert(first.is_in_group("enemies") and second.is_in_group("enemies")) print("PASS: passive extension rolls, persistence, pickup, armor and virus combat/lifecycle") for sound in get_tree().root.find_children("*", "AudioStreamPlayer", true, false) + get_tree().root.find_children("*", "AudioStreamPlayer2D", true, false): sound.stop() sound.stream = null preload("res://scripts/game/weapon_audio.gd")._streams.clear() await get_tree().process_frame await get_tree().process_frame get_tree().quit()