extends GutTest ## The XP curve and what a level is worth. Pure arithmetic, so the shape of ## progression can be argued about without running a server. func test_level_one_is_the_base_health() -> void: assert_eq(Progression.max_hp_for_level(1), SimConfig.PLAYER_MAX_HP) func test_each_level_adds_ten_health() -> void: for level in range(1, Progression.MAX_LEVEL): assert_eq(Progression.max_hp_for_level(level + 1) - Progression.max_hp_for_level(level), Progression.HP_PER_LEVEL) func test_the_cap_is_reachable_and_final() -> void: assert_eq(Progression.max_hp_for_level(Progression.MAX_LEVEL), SimConfig.PLAYER_MAX_HP + (Progression.MAX_LEVEL - 1) * Progression.HP_PER_LEVEL) # Past the cap nothing further is gained, however much xp arrives. assert_eq(Progression.max_hp_for_level(999), Progression.max_hp_for_level(Progression.MAX_LEVEL)) assert_eq(Progression.xp_to_next(Progression.MAX_LEVEL), 0) func test_level_is_derived_consistently_from_experience() -> void: # The two directions must agree exactly, or a character's level could # disagree with the experience that earned it. for level in range(1, Progression.MAX_LEVEL + 1): var at := Progression.total_xp_for_level(level) assert_eq(Progression.level_for_xp(at), level, "exactly enough xp for level %d should be level %d" % [level, level]) # Skipped at level 1: there is no level 0 to fall short into, and # negative lifetime xp is not a state a character can be in. if level > Progression.START_LEVEL: assert_eq(Progression.level_for_xp(at - 1), level - 1, "one short of level %d should still be level %d" % [level, level - 1]) func test_experience_never_exceeds_the_cap() -> void: assert_eq(Progression.level_for_xp(99999999), Progression.MAX_LEVEL) assert_eq(Progression.level_progress(99999999), 1.0, "a capped character's bar should read full, not empty") func test_progress_runs_from_zero_to_one_within_a_level() -> void: var at := Progression.total_xp_for_level(3) assert_almost_eq(Progression.level_progress(at), 0.0, 0.001) assert_almost_eq(Progression.level_progress(at + Progression.xp_to_next(3) / 2), 0.5, 0.05) func test_the_requirement_grows_with_level() -> void: for level in range(1, Progression.MAX_LEVEL - 1): assert_gt(Progression.xp_to_next(level + 1), Progression.xp_to_next(level), "later levels must cost more, or the curve is flat at the end") ## The brief's one concrete pacing requirement: a first full clear should be a ## bit more than the first level-up needs. func test_a_first_full_dungeon_clear_slightly_exceeds_the_first_level() -> void: # A depth-1 dungeon holds roughly a dozen enemies plus the boss. var trash := 12 * Progression.xp_for_enemy(Content.ENEMY_DRIFTER) var clear := trash + Progression.xp_for_boss(Content.BOSS_WARDEN) var needed := Progression.xp_to_next(1) assert_gt(clear, needed, "a full clear should get you the first level") assert_lt(clear, needed * 2, "but not two levels, or the first run outpaces the curve") func test_unknown_enemies_award_nothing_rather_than_a_default() -> void: assert_eq(Progression.xp_for_enemy(&"no_such_enemy"), 0) assert_eq(Progression.xp_for_enemy(Content.ENEMY_DUMMY), 0, "the practice target must not be an xp farm") func test_experience_below_the_first_level_is_still_level_one() -> void: assert_eq(Progression.level_for_xp(0), Progression.START_LEVEL) assert_eq(Progression.level_for_xp(-500), Progression.START_LEVEL, "a nonsensical total must clamp rather than produce a level 0 character")