extends RefCounted const ORBS := preload("res://scripts/items/special_orbs.gd") const LOOT := preload("res://scripts/game/distant_loot.gd") const GENERATOR := preload("res://scripts/items/item_generator.gd") var failures: Array[String] = [] class HitTarget extends Node2D: var damage_type := -1 var damage := 0.0 func take_piercing_weapon_hit(amount: float, _critical: bool, kind: int, _piercing: float) -> void: damage_type = kind damage = amount func check(value: bool, message: String) -> void: if not value: failures.append(message) func run(game: Node) -> Array[String]: var player: Node = game.player var inventory: ShipInventory = player.get_node("ShipInventory") var progress: Resource = GameState.captain.expedition player.is_docked = true for id in ORBS.IDS: progress.orbs[id] = 10 check(ORBS.icon(id) != null, "Missing orb icon: " + id) var weapon := GENERATOR.generate_for_definition(preload("res://resources/items/plasma_cannon.tres"),1) var addon := ItemStatModifier.new() addon.stat_key = "attack_speed_percent" addon.value = 25.0 addon.affix = preload("res://resources/items/affixes/attack_speed_percent.tres") weapon.additional_modifiers = [addon] inventory.weapon_slots[0] = weapon check(is_equal_approx(weapon.get_fire_interval(), weapon.definition.fire_interval / 1.25), "Attack speed interval") var cost := weapon.get_energy_cost() addon.value = 500.0 check(is_equal_approx(weapon.attack_speed_multiplier(),1.25), "Attack speed capped") addon.value = 25.0 check(is_equal_approx(cost, weapon.get_energy_cost()), "Attack speed does not reduce shot cost") for level in [1,120,1000]: var bounds := addon.affix.get_value_range(level) check(bounds.x >= 2.0 and bounds.y <= 25.0, "Bounded attack speed rolls") check(not addon.affix.can_roll(preload("res://resources/items/reinforced_shield.tres"),1), "Attack speed only rolls on weapons") var slow_dps := inventory.get_weapon_dps(0) / 1.25 addon.value = 0.0 check(is_equal_approx(inventory.get_weapon_dps(0),slow_dps), "DPS reflects attack speed") addon.value = 25.0 var shield := GENERATOR.generate_for_definition(preload("res://resources/items/reinforced_shield.tres"),1) var engine := GENERATOR.generate_for_definition(preload("res://resources/items/engine_extension.tres"),1) shield.additional_modifiers = [] engine.additional_modifiers = [] inventory.module_slots[0] = shield inventory.module_slots[2] = engine check(not ORBS.accepts(weapon,"polarization") and not ORBS.accepts(shield,"atomic_engine"), "Orb category restrictions") check(not ORBS.accepts(engine,"electricity"), "Damage orb rejects engine") var detached := GENERATOR.generate_for_definition(weapon.definition,1) check(not ORBS.apply(progress,inventory,detached,"replication"), "Cannot replicate unowned equipment") player.is_docked = false check(not ORBS.apply(progress,inventory,weapon,"electricity"), "Orbs require docking") player.is_docked = true var original_damage := weapon.get_damage_range() for id in ORBS.DAMAGE_ORBS: check(ORBS.apply(progress,inventory,weapon,id), "Apply damage orb: " + id) var kind: int = {"electricity":DamageTypes.Type.ENERGY,"radiation":DamageTypes.Type.RADIATION,"kinetic":DamageTypes.Type.KINETIC}[id] check(weapon.get_damage_type() == kind, "Converted weapon damage type") check(weapon.get_damage_range() == original_damage, "Conversion preserves damage") check(not ORBS.apply(progress,inventory,weapon,id), "Same damage orb cannot stack") var target := HitTarget.new() game.add_child(target) player.get_node("WeaponController")._apply_damage(target,100,false,0,weapon.get_damage_type()) check(target.damage_type == kind, "Laser uses converted type") var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() projectile.damage = 100.0 projectile.damage_type_override = weapon.get_damage_type() projectile._apply_damage_once(target) check(target.damage_type == kind and target.damage == 100.0, "Projectile uses converted type") projectile.free() target.free() check(weapon.special_orbs.size() == 1, "Damage orbs replace each other") var normal_boost_cost: float = player.get_mini_boost_energy_cost() check(ORBS.apply(progress,inventory,engine,"atomic_engine"), "Atomic engine applied") check(is_equal_approx(player.get_mini_boost_energy_cost(),normal_boost_cost*0.5), "Atomic engine halves TAB cost") player.energy = 50.0 Input.action_press("ship_mini_boost") player._update_mini_boost(1.0) Input.action_release("ship_mini_boost") check(is_equal_approx(player.energy,50.0-normal_boost_cost*0.5), "Actual atomic boost consumes half energy") check(ORBS.apply(progress,inventory,engine,"booster"), "Free booster stacks by precedence") check(is_zero_approx(player.get_mini_boost_energy_cost()), "Free booster remains free") check(ORBS.apply(progress,inventory,shield,"polarization"), "Polarization applied") player.is_docked = false player.absorption = Vector3.ZERO player.max_energy = 10000.0 player.energy = 0.0 player.max_shields = 1000.0 player.shields = 1000.0 for attempt in 100: player.take_damage(1.0) if player.shields < 1000.0: break check(is_equal_approx(player.energy,1.0), "Polarization cannot turn a tiny hit into excess energy") player.energy = player.max_energy - 1.0 player.shields = 1000.0 for attempt in 100: player.take_damage(1.0) if player.shields < 1000.0: break check(is_equal_approx(player.energy,player.max_energy), "Polarization respects generator capacity") player.shields = 0.0 player.energy = 0.0 player.hull = 10000.0 player.take_damage(1.0) check(is_zero_approx(player.energy), "Hull-only hit gives no polarization energy") player.is_docked = true check(ORBS.apply(progress,inventory,weapon,"replication"), "Replication applied") var copy: ItemInstance = progress.freighter_storage.back() check(copy != weapon and copy.definition == weapon.definition, "Replica is distinct equipment") check(copy.get_damage_range() == weapon.get_damage_range() and copy.special_orbs == weapon.special_orbs, "Replica preserves rolls and orbs") copy.additional_modifiers[0].value = 1.0 copy.special_orbs.clear() check(is_equal_approx(weapon.additional_modifiers[0].value,25.0) and weapon.special_orbs.size()==1, "Replica changes cannot mutate original") var missile := GENERATOR.generate_for_definition(preload("res://resources/items/missile_launcher.tres"),1) missile.additional_modifiers = [addon] check(is_equal_approx(inventory.get_missile_cooldown(missile),4.0), "Attack speed affects missiles") addon.value = 0.0 for seed_value in 100: var no_rare := LOOT.roll(120,true,seed_value,1.0,0.05) var rare := LOOT.roll(120,true,seed_value,1.0,0.049) check(no_rare.size()==2 and no_rare[1].orb != "replication", "Replication excluded from guaranteed pool") check(rare.size()==3 and rare[2].orb == "replication", "Separate five percent replication drop") check(LOOT.roll(120,false,1,1.0,0.0).is_empty(), "Regular enemies cannot drop replication") return failures