143 lines
6.3 KiB
GDScript
143 lines
6.3 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 _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()
|