ci / verify (push) Successful in 53s
At 331 of 1500 health the bar read as nearly full. The pack's bar sprite is light all the way through, so the EMPTY part was the brightest thing on it. Every bar now has a dark track inside its frame, scaled with the bar's height -- a fixed inset left the thin experience bar four pixels of track and six of white -- so empty reads as empty. Health carries its number inside the bar, and a damage trail: the part the last hit took stays in a darker red for a moment and then drains, so a hit reads as an amount rather than as a bar that is suddenly shorter. Two quick hits read as one, measured from where the first began. The trail was pale at first and the number vanished against it; darker fixed that. Experience flashes and shows "+N xp" on a kill, summed across a burst so it does not flicker. Every block of HUD text sits on a dark panel, since the floor art is busy enough to beat a text shadow. Centre banners for the moments that change what you should do: a level gained, a boss changing phase (not the phase it was found in), and the escape channel -- which was a second bar in the corner and is now one bar where you are looking, with the key to keep holding. Party members you cannot see get a marker at the edge of sight pointing the way to them. Empty inventory slots step back, so the blank parchment square is no longer the most prominent thing in the row. The connect screen opens on a real boss pattern behind its panel -- the bosses' own emitters, fired into a pool and drawn by the game's own bullet renderer, dimmed. It is primed with five seconds of ticks: every pattern starts at the centre of the screen, which is where the panel is, so unprimed it showed nothing for its first second or two. The title is spaced and underlined, Credits is on the screen itself, Join says Enter works and has focus, the port is validated before it can fail, and "connecting" animates so a slow attempt does not look frozen. The boss-bar logic is pulled out into track_boss() so it can be tested: it waits for the fog to light the boss, stays for the fight, has to be seen again in the next dungeon -- every dungeon's boss is actor 1, so the id alone would have carried "seen" across -- and announces only real phase changes. screenshot.tscn captures the escape banner by progress rather than by frame count, because the channel is server ticks and a completed escape teleports the player out mid-sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
7.1 KiB
GDScript
214 lines
7.1 KiB
GDScript
extends GutTest
|
|
## HUD and connect-screen behaviour that does not need a screen to check.
|
|
## What things LOOK like is screenshot.tscn's job; this is what they DO.
|
|
|
|
const HUD := preload("res://src/ui/hud.gd")
|
|
const MENU := preload("res://src/ui/main_menu.gd")
|
|
const BACKDROP := preload("res://src/ui/menu_backdrop.gd")
|
|
|
|
|
|
func _hud_with(hp: int, max_hp: int) -> Array:
|
|
var hud: CanvasLayer = HUD.new()
|
|
add_child_autofree(hud)
|
|
var c: ClientRuntime = autofree(ClientRuntime.new())
|
|
c.my_hp = hp
|
|
c.my_max_hp = max_hp
|
|
hud.client = c
|
|
hud._hp_trail = float(hp) / float(max_hp)
|
|
hud._hp_last = hud._hp_trail
|
|
return [hud, c]
|
|
|
|
|
|
# --- The damage trail -------------------------------------------------------
|
|
|
|
func test_a_hit_leaves_a_trail_where_the_bar_was() -> void:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
c.my_hp = 400
|
|
hud._watch_health(0.016)
|
|
assert_almost_eq(hud._hp_trail, 1.0, 0.001, "the trail holds at the old value")
|
|
assert_almost_eq(hud._hp_last, 0.4, 0.001)
|
|
|
|
|
|
func test_the_trail_holds_and_then_drains_to_meet_the_bar() -> void:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
c.my_hp = 400
|
|
hud._watch_health(0.016)
|
|
# Within the hold, nothing moves -- that pause is what makes the amount
|
|
# readable.
|
|
for _i in int(HUD.TRAIL_HOLD / 0.016) - 2:
|
|
hud._watch_health(0.016)
|
|
assert_almost_eq(hud._hp_trail, 1.0, 0.001, "still holding")
|
|
for _i in 200:
|
|
hud._watch_health(0.016)
|
|
assert_almost_eq(hud._hp_trail, 0.4, 0.001, "and then drained all the way down")
|
|
|
|
|
|
func test_healing_leaves_no_trail() -> void:
|
|
var pair := _hud_with(400, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
c.my_hp = 900
|
|
hud._watch_health(0.016)
|
|
assert_almost_eq(hud._hp_trail, 0.9, 0.001,
|
|
"a trail is for what was lost; on a heal the bar and trail move together")
|
|
|
|
|
|
func test_a_second_hit_during_the_hold_keeps_the_original_top() -> void:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
c.my_hp = 700
|
|
hud._watch_health(0.016)
|
|
c.my_hp = 300
|
|
hud._watch_health(0.016)
|
|
assert_almost_eq(hud._hp_trail, 1.0, 0.001,
|
|
"two quick hits read as one big one, measured from where it started")
|
|
|
|
|
|
# --- Experience -------------------------------------------------------------
|
|
|
|
func test_experience_arriving_quickly_is_summed_into_one_number() -> void:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
hud._seen_character = "a"
|
|
c.my_total_xp = 100
|
|
hud._watch_experience(0.016)
|
|
c.my_total_xp = 112
|
|
hud._watch_experience(0.016)
|
|
c.my_total_xp = 120
|
|
hud._watch_experience(0.016)
|
|
assert_eq(hud._xp_gain, 20, "+12 then +8 shows as +20, not as a flicker")
|
|
|
|
|
|
func test_the_first_sight_of_a_character_is_not_a_gain() -> void:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
c.my_total_xp = 5000
|
|
hud._watch_experience(0.016)
|
|
assert_eq(hud._xp_flash, 0.0, "logging in with 5000 experience did not just earn it")
|
|
|
|
|
|
# --- The connect screen -----------------------------------------------------
|
|
|
|
func _menu() -> Control:
|
|
var m: Control = MENU.new()
|
|
add_child_autofree(m)
|
|
return m
|
|
|
|
|
|
func test_a_port_that_cannot_connect_disables_join_and_says_why() -> void:
|
|
var m := _menu()
|
|
m._port.text = "twenty"
|
|
m._validate()
|
|
assert_true(m._join_button.disabled)
|
|
assert_true(m._host_button.disabled)
|
|
assert_string_contains(m._status.text, "port")
|
|
m._port.text = "70000"
|
|
m._validate()
|
|
assert_true(m._join_button.disabled, "out of range is as useless as not a number")
|
|
m._port.text = "27015"
|
|
m._validate()
|
|
assert_false(m._join_button.disabled)
|
|
assert_eq(m._status.text, "", "and the complaint goes when the problem does")
|
|
|
|
|
|
func test_no_address_disables_join_but_not_host() -> void:
|
|
var m := _menu()
|
|
m._address.text = " "
|
|
m._validate()
|
|
assert_true(m._join_button.disabled, "nowhere to join")
|
|
assert_false(m._host_button.disabled, "hosting needs no address")
|
|
|
|
|
|
func test_busy_wins_over_valid() -> void:
|
|
var m := _menu()
|
|
m.set_busy(true)
|
|
assert_true(m._join_button.disabled, "a connection in flight is not a second invitation")
|
|
m.set_busy(false)
|
|
assert_false(m._join_button.disabled)
|
|
|
|
|
|
func test_the_connect_screen_asks_for_no_name() -> void:
|
|
# It used to, and the answer went nowhere a person could see -- it only named
|
|
# the characters bots create.
|
|
var m := _menu()
|
|
for node in m.find_children("*", "LineEdit", true, false):
|
|
assert_ne((node as LineEdit).placeholder_text, "name")
|
|
assert_eq(m.find_children("*", "LineEdit", true, false).size(), 2,
|
|
"an address and a port, nothing else")
|
|
|
|
|
|
func test_the_backdrop_opens_on_a_pattern_already_in_flow() -> void:
|
|
# Every pattern starts at the centre of the screen, behind the panel. Primed,
|
|
# the first frame already has bullets well clear of it.
|
|
var b: Node2D = BACKDROP.new()
|
|
add_child_autofree(b)
|
|
var centre := b.get_viewport_rect().size * 0.5
|
|
var clear_of_panel := 0
|
|
for i in b._pool.high_water:
|
|
if b._pool.alive[i] != 0 and b._pool.pos[i].distance_to(centre) > 320.0:
|
|
clear_of_panel += 1
|
|
assert_gt(clear_of_panel, 10, "bullets beyond the panel on the very first frame")
|
|
|
|
|
|
# --- The boss bar -----------------------------------------------------------
|
|
|
|
## A HUD whose fog sees from the left of a room, with a wall down the middle.
|
|
func _hud_with_sight() -> Array:
|
|
var pair := _hud_with(1000, 1000)
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
var g := MapGrid.new(40, 20, MapGrid.Kind.FLOOR)
|
|
for ty in 20:
|
|
g.set_tile(20, ty, MapGrid.Kind.WALL)
|
|
var sight := Visibility.new()
|
|
sight.compute(g, Vector2(10.5, 10.5) * MapGrid.TILE, 460.0)
|
|
hud.sight = sight
|
|
c.instance_id = 3
|
|
c.boss_def = Content.boss(&"warden")
|
|
return [hud, c]
|
|
|
|
|
|
func _boss_at(x_tiles: float, phase: int = 0) -> Dictionary:
|
|
return {"id": 1, "pos": Vector2(x_tiles, 10.5) * MapGrid.TILE, "phase": phase, "hp": 100}
|
|
|
|
|
|
func test_the_bar_waits_until_the_boss_has_been_seen() -> void:
|
|
var hud: CanvasLayer = _hud_with_sight()[0]
|
|
# Behind the wall: in the snapshot -- the interest radius passes through
|
|
# walls -- but not seen.
|
|
assert_false(hud.track_boss(_boss_at(24.0)), "known about is not the same as seen")
|
|
assert_true(hud.track_boss(_boss_at(16.0)), "in the light, it shows")
|
|
|
|
|
|
func test_once_seen_it_stays_for_the_fight() -> void:
|
|
var hud: CanvasLayer = _hud_with_sight()[0]
|
|
hud.track_boss(_boss_at(16.0))
|
|
assert_true(hud.track_boss(_boss_at(24.0)),
|
|
"stepping behind a pillar must not take the bar away")
|
|
|
|
|
|
func test_a_new_dungeon_has_to_be_seen_again() -> void:
|
|
var pair := _hud_with_sight()
|
|
var hud: CanvasLayer = pair[0]
|
|
var c: ClientRuntime = pair[1]
|
|
hud.track_boss(_boss_at(16.0))
|
|
# Every dungeon's boss is actor 1, so the id alone would carry the last
|
|
# fight's "seen" straight into the next one.
|
|
c.instance_id = 4
|
|
assert_false(hud.track_boss(_boss_at(24.0)))
|
|
|
|
|
|
func test_a_phase_change_is_announced_and_the_first_phase_is_not() -> void:
|
|
var hud: CanvasLayer = _hud_with_sight()[0]
|
|
hud.track_boss(_boss_at(16.0, 1))
|
|
assert_eq(hud._banner_title, "", "the phase it was in when found is not a change")
|
|
hud.track_boss(_boss_at(16.0, 2))
|
|
assert_eq(hud._banner_title, Content.boss(&"warden").phases[2].name.to_upper())
|