extends Node2D const CATALOG := preload("res://scripts/captain/captain_skills.gd") const GENERATOR := preload("res://scripts/items/item_generator.gd") const EXPLORATION := preload("res://scripts/levels/exploration_generator.gd") const PLAYER := preload("res://scenes/game/player_ship.tscn") const DEFINITION := preload("res://resources/items/plasma_cannon.tres") var ship: CharacterBody2D func snapshot() -> Dictionary: var container := Node2D.new() add_child(container) EXPLORATION.spawn(container, {"planets": []}, ship) var result := {} for poi in container.get_children(): result[poi.get("poi_id")] = {"kind": poi.get("kind"), "position": poi.position, "reward_seed": poi.get("reward_seed")} container.free() return result func count_kind(data: Dictionary, kind: String) -> int: var count := 0 for poi in data.values(): if poi["kind"] == kind: count += 1 return count func _ready() -> void: GameState.start_new_run() ship = PLAYER.instantiate() as CharacterBody2D ship.name = "PlayerShip" add_child(ship) ship.set_physics_process(false) ship.get_node("WeaponController").set_physics_process(false) assert(CATALOG.IDS.has("lucky") and CATALOG.IDS.has("explorer")) for id in ["lucky", "explorer"]: assert(not CATALOG.DATA[id]["active"]) assert(CATALOG.icon(id) != null) assert(not CATALOG.description(id, 3).is_empty()) # Every roll threshold is exact, with 0% baseline and a hard level cap. for rank in range(4): GameState.captain.skills["lucky"] = rank var chance: float = [0.0, 0.05, 0.1, 0.25][rank] assert(is_equal_approx(GameState.captain.get_skill_bonus("lucky"), chance)) for roll in 1000: assert(GameState.captain.roll_loot_level(10, float(roll) / 1000.0) == (11 if float(roll) / 1000.0 < chance else 10)) assert(GameState.captain.roll_loot_level(1000, 0.0) == 1000) assert(GameState.captain.roll_loot_level(0, 0.99) == 1) # Exercise actual drops, including stat scaling and the normal level generator. seed(888) var upgrades := 0 for index in 2000: var item := GENERATOR.generate_loot(10, 0, DEFINITION) assert(item.item_level in [10, 11]) if item.item_level == 11: upgrades += 1 assert(upgrades == 0 if rank == 0 else absf(float(upgrades) / 2000.0 - chance) < 0.04) assert(GENERATOR.generate_for_definition(DEFINITION, 10).item_level == 10) # Both new skills participate in the normal level-up offer and training path. for id in ["lucky", "explorer"]: var captain := CaptainProgress.new() for skill in CATALOG.IDS: captain.skills[skill] = 3 captain.skills[id] = 2 captain.learning_points = 1 captain.prepare_skill_offers() assert(captain.skill_offers == [id]) assert(captain.train_skill(id)) assert(captain.get_skill_rank(id) == 3) # Each rank adds exactly 1/3/6 wrecks and leaves every base POI intact. var baseline := {} for rank in range(4): GameState.start_new_run() GameState.system_seed = 222 GameState.captain.skills["explorer"] = rank var current := snapshot() if rank == 0: baseline = current var bonus: int = [0, 1, 3, 6][rank] assert(count_kind(current, "wreck") == count_kind(baseline, "wreck") + bonus) assert(count_kind(current, "asteroid") == count_kind(baseline, "asteroid")) for id in baseline: assert(current[id] == baseline[id]) GameState.start_new_run() GameState.system_seed = 222 var before := snapshot() GameState.captain.skills["explorer"] = 3 GameState.captain.emit_changed() assert(snapshot() == before) # Reloading the current board does not add wrecks. GameState.system_seed = 333 var next_board := snapshot() var rng := RandomNumberGenerator.new() rng.seed = 333 ^ 0x504f49 assert(count_kind(next_board, "wreck") == rng.randi_range(2, 4) + 6) GameState.explored_pois["333:1000"] = true var reloaded := snapshot() assert(not reloaded.has("333:1000")) assert(reloaded.size() == next_board.size() - 1) assert(GameState.exploration_wreck_bonuses[333] == 6) GameState.start_new_run() assert(GameState.exploration_wreck_bonuses.is_empty()) print("LUCKY_EXPLORER_TEST: PASS (0/5/10/25% luck, thresholds, 8000 drops, level cap/scaling, skill offers/training, +1/3/6 wrecks, stable base POIs, next-board application, reload/claimed-wreck persistence)") get_tree().quit()