Randoloot/tests/abilities/passive_extensions_test.gd
2026-10-17 21:28:08 +02:00

165 lines
7.4 KiB
GDScript

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 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()
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)
# Duplicate extensions stay in cargo; replacing and moving equipped items is allowed.
recycler.passive_value = 40
var duplicate := GENERATOR.generate_for_definition(load("res://resources/items/recycler.tres"), 1)
assert(inventory.add_item(duplicate))
var cargo_index := inventory.cargo.find(duplicate)
assert(not inventory.can_move_item("cargo", cargo_index, "extension", 3))
assert(not inventory.move_item("cargo", cargo_index, "extension", 3))
assert(inventory.extension_slots[3] == null and inventory.cargo[cargo_index] == duplicate)
assert(inventory.move_item("extension", 0, "extension", 3))
assert(inventory.move_item("extension", 3, "extension", 0))
assert(inventory.move_item("cargo", cargo_index, "extension", 0))
assert(inventory.extension_slots[0] == duplicate and inventory.cargo[cargo_index] == recycler)
assert(inventory.move_item("cargo", cargo_index, "extension", 0))
assert(inventory.move_item("extension", 0, "extension", 1))
assert(inventory.move_item("extension", 1, "extension", 0))
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
seed_successful_infection()
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() == ship)
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
seed_successful_infection()
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()