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>
This commit is contained in:
1 parent
3d1b5da72b
commit
6e872e2cf5
8 files changed
+703
-96
No files matched your search
+276
-64
@@ -39,6 +39,23 @@ const LOW_HEALTH := 0.35
|
||||
const HINT_HOLD := 6.0
|
||||
const HINT_FADE := 1.5
|
||||
|
||||
## Health bar, taller than the others because it carries its own numbers.
|
||||
const HP_H := 20.0
|
||||
## Experience is a thin strip under health: it matters between fights, not in
|
||||
## them.
|
||||
const XP_H := 10.0
|
||||
## How long the damage trail holds before draining, and how fast it drains (as
|
||||
## a share of the bar per second). Long enough to read what the hit took.
|
||||
const TRAIL_HOLD := 0.45
|
||||
const TRAIL_DRAIN := 0.9
|
||||
## Centre-screen banners: how long one stays, and its fade at each end.
|
||||
const BANNER_LIFE := 2.4
|
||||
const BANNER_FADE := 0.5
|
||||
## The inside of every bar. The pack's bar sprite is light all the way through,
|
||||
## so on its own an EMPTY bar was the brightest thing on screen and a player on
|
||||
## a fifth of their health saw a bar that read as nearly full.
|
||||
const COL_TRACK := Color(0.07, 0.07, 0.11)
|
||||
|
||||
const COL_TEXT := Color(0.86, 0.9, 0.97)
|
||||
const COL_DIM := Color(0.58, 0.63, 0.74)
|
||||
const COL_HEALTH := Color(0.35, 0.9, 0.6)
|
||||
@@ -61,6 +78,25 @@ var _hit_flash: float = 0.0
|
||||
var client: ClientRuntime = null
|
||||
|
||||
var _toasts: Array[Dictionary] = []
|
||||
## The damage trail: where the health bar was before the last hit, held a
|
||||
## moment and then drained down to where it is now.
|
||||
var _hp_trail: float = 1.0
|
||||
var _hp_last: float = 1.0
|
||||
var _hp_hold: float = 0.0
|
||||
## Experience just gained, shown beside the bar for a moment.
|
||||
var _xp_last: int = -1
|
||||
var _xp_gain: int = 0
|
||||
var _xp_flash: float = 0.0
|
||||
## The banner currently shown, or an empty title.
|
||||
var _banner_title := ""
|
||||
var _banner_sub := ""
|
||||
var _banner_col := Color.WHITE
|
||||
var _banner_born: float = -100.0
|
||||
## The boss phase last announced, keyed like _boss_seen.
|
||||
var _boss_phase_key := ""
|
||||
var _boss_phase: int = -1
|
||||
## Dark backing for every panel of text over the world. Built once.
|
||||
var _panel_style: StyleBoxFlat = null
|
||||
## Which boss, in which instance, has been seen -- see _draw_boss_bar.
|
||||
var _boss_seen := ""
|
||||
## The level last drawn and whose it was, so a level-up can be announced the
|
||||
@@ -113,6 +149,8 @@ func _process(delta: float) -> void:
|
||||
_instance_seen = client.instance_id
|
||||
_instance_since = now
|
||||
_watch_level()
|
||||
_watch_health(delta)
|
||||
_watch_experience(delta)
|
||||
_update_respawn_button()
|
||||
_canvas.queue_redraw()
|
||||
|
||||
@@ -134,6 +172,43 @@ func flash_hit() -> void:
|
||||
_hit_flash = 1.0
|
||||
|
||||
|
||||
## A line across the middle of the screen for a moment: a level gained, a boss
|
||||
## changing phase. Things that change what you should do next, which is why
|
||||
## they are not in the message feed with the potions.
|
||||
func banner(title: String, sub: String = "", colour: Color = COL_GOLD) -> void:
|
||||
_banner_title = title
|
||||
_banner_sub = sub
|
||||
_banner_col = colour
|
||||
_banner_born = _now()
|
||||
|
||||
|
||||
## Keep the damage trail: when health drops, the trail stays where it was for
|
||||
## TRAIL_HOLD and then drains to meet the bar. Healing moves both at once.
|
||||
func _watch_health(delta: float) -> void:
|
||||
var frac := clampf(float(client.my_hp) / float(maxi(client.my_max_hp, 1)), 0.0, 1.0)
|
||||
if frac < _hp_last - 0.0001:
|
||||
_hp_trail = maxf(_hp_trail, _hp_last)
|
||||
_hp_hold = TRAIL_HOLD
|
||||
if frac >= _hp_trail:
|
||||
_hp_trail = frac
|
||||
if _hp_hold > 0.0:
|
||||
_hp_hold -= delta
|
||||
else:
|
||||
_hp_trail = move_toward(_hp_trail, frac, TRAIL_DRAIN * delta)
|
||||
_hp_last = frac
|
||||
|
||||
|
||||
func _watch_experience(delta: float) -> void:
|
||||
_xp_flash = maxf(_xp_flash - delta, 0.0)
|
||||
var xp := client.my_total_xp
|
||||
if _xp_last >= 0 and xp > _xp_last and String(_seen_character) != "":
|
||||
# Summed while the flash is up, so a burst of kills reads as one total
|
||||
# rather than as a number that flickers.
|
||||
_xp_gain = (_xp_gain if _xp_flash > 0.0 else 0) + (xp - _xp_last)
|
||||
_xp_flash = 1.4
|
||||
_xp_last = xp
|
||||
|
||||
|
||||
## Announce a level the moment the snapshot's experience crosses one. Watched
|
||||
## rather than signalled because experience rides the snapshot, and the first
|
||||
## sight of a character -- logging in, swapping -- must not read as a level-up.
|
||||
@@ -146,11 +221,15 @@ func _watch_level() -> void:
|
||||
if String(who["id"]) != _seen_character:
|
||||
_seen_character = String(who["id"])
|
||||
_seen_level = level
|
||||
# A new character is not a hit, and its experience is not a gain.
|
||||
_hp_trail = clampf(float(client.my_hp) / float(maxi(client.my_max_hp, 1)), 0.0, 1.0)
|
||||
_hp_last = _hp_trail
|
||||
_xp_last = client.my_total_xp
|
||||
return
|
||||
if level > _seen_level:
|
||||
var where := "" if client.instance_kind == Protocol.InstanceKind.LOBBY \
|
||||
else " -- a choice waits at the quartermaster"
|
||||
notify("LEVEL %d%s" % [level, where], COL_GOLD)
|
||||
banner("LEVEL %d" % level,
|
||||
"a choice waits at the quartermaster" if level < Progression.MAX_LEVEL
|
||||
else "the highest there is", COL_GOLD)
|
||||
_seen_level = level
|
||||
|
||||
|
||||
@@ -243,11 +322,11 @@ func _draw_hud() -> void:
|
||||
# Under everything, so the edge of the screen colours without dimming any of
|
||||
# the text on top of it.
|
||||
_draw_vignette()
|
||||
_draw_party_markers()
|
||||
|
||||
var y := _draw_player_panel()
|
||||
_draw_level_up_notice(y)
|
||||
_draw_player_panel()
|
||||
_draw_boss_bar()
|
||||
_draw_arriving()
|
||||
_draw_banner()
|
||||
_draw_cleared_countdown()
|
||||
_draw_others()
|
||||
_draw_inventory()
|
||||
@@ -260,13 +339,37 @@ func _draw_hud() -> void:
|
||||
_centred(_canvas.size.y * 0.5, "DOWN", 34, Color(1.0, 0.4, 0.4))
|
||||
|
||||
|
||||
## Who you are, where you are, and how you are doing. Returns the y below the
|
||||
## last thing drawn, so whatever comes next stacks rather than overlaps.
|
||||
func _draw_player_panel() -> float:
|
||||
## A dark, slightly translucent box behind a block of HUD text. The floor art is
|
||||
## busy and mid-toned; text straight over it was readable on some tiles and not
|
||||
## on others, and the shadow under each letter only went so far.
|
||||
func _panel(rect: Rect2) -> void:
|
||||
if _panel_style == null:
|
||||
_panel_style = StyleBoxFlat.new()
|
||||
_panel_style.bg_color = Color(0.05, 0.05, 0.09, 0.62)
|
||||
_panel_style.border_color = Color(1.0, 1.0, 1.0, 0.07)
|
||||
_panel_style.set_border_width_all(1)
|
||||
_panel_style.set_corner_radius_all(4)
|
||||
_canvas.draw_style_box(_panel_style, rect)
|
||||
|
||||
|
||||
## Who you are, where you are, and how you are doing.
|
||||
##
|
||||
## Laid out in two passes -- measure the rows, draw the panel behind them, then
|
||||
## draw them -- because the panel has to be under the text and its height
|
||||
## depends on which rows are showing.
|
||||
func _draw_player_panel() -> void:
|
||||
var who := client.current_character()
|
||||
var has_character := not who.is_empty()
|
||||
var rows := 20.0 + HP_H + 6.0
|
||||
if has_character:
|
||||
rows += 22.0 + XP_H + 18.0
|
||||
if client.upgrades_pending > 0:
|
||||
rows += 20.0
|
||||
_panel(Rect2(MARGIN - 10.0, MARGIN - 8.0, BAR_W + 20.0, rows + 12.0))
|
||||
|
||||
var x := MARGIN
|
||||
var y := MARGIN
|
||||
var who := client.current_character()
|
||||
if not who.is_empty():
|
||||
if has_character:
|
||||
var name_text := String(who["name"])
|
||||
_text(Vector2(x, y + 15.0), name_text, 17, COL_TEXT)
|
||||
_text(Vector2(x + _text_width(name_text, 17) + 10.0, y + 15.0),
|
||||
@@ -288,41 +391,44 @@ func _draw_player_panel() -> float:
|
||||
# Red when low, not a blend toward it: green-to-red through the middle is
|
||||
# brown, which reads as a colour rather than as a warning.
|
||||
var hp_tint := COL_HEALTH if frac >= LOW_HEALTH else Color(1.0, 0.38, 0.36)
|
||||
_bar(Vector2(x, y), frac, hp_tint)
|
||||
_text(Vector2(x + BAR_W + 10.0, y + 13.0),
|
||||
"%d / %d" % [client.my_hp, client.my_max_hp], 12, hp_tint)
|
||||
y += BAR_H + 4.0
|
||||
_bar(Vector2(x, y), frac, hp_tint, _hp_trail, Vector2(BAR_W, HP_H))
|
||||
# Inside the bar, where the eye already is. Light on the dark track and on
|
||||
# every fill colour, which a number beside the bar did not need to be.
|
||||
_text(Vector2(x, y + HP_H - 5.0), "%d / %d" % [client.my_hp, client.my_max_hp],
|
||||
12, COL_TEXT, HORIZONTAL_ALIGNMENT_CENTER, BAR_W)
|
||||
y += HP_H + 6.0
|
||||
|
||||
if _draw_xp_bar(Vector2(x, y)):
|
||||
y += BAR_H + 4.0
|
||||
if has_character:
|
||||
_draw_xp_bar(Vector2(x, y))
|
||||
y += XP_H + 18.0
|
||||
|
||||
if client.my_escaping:
|
||||
_bar(Vector2(x, y), client.my_escape, Color(0.5, 0.85, 1.0))
|
||||
_text(Vector2(x + BAR_W + 10.0, y + 13.0), "ESCAPING", 12,
|
||||
Color(0.5, 0.85, 1.0))
|
||||
y += BAR_H + 4.0
|
||||
return y
|
||||
# No escape row here: the channel is a banner in the middle of the screen,
|
||||
# where you are looking, and a second bar for the same second was noise.
|
||||
_draw_level_up_notice(y)
|
||||
|
||||
|
||||
## A thin bar under health: progress toward the next level, and the level
|
||||
## itself. Drawn from the server's numbers, never recomputed locally. Returns
|
||||
## whether it drew, since a player at the roster screen has no character.
|
||||
func _draw_xp_bar(at: Vector2) -> bool:
|
||||
if client.current_character().is_empty():
|
||||
return false
|
||||
## A thin bar under health: progress toward the next level. Drawn from the
|
||||
## server's numbers, never recomputed locally.
|
||||
func _draw_xp_bar(at: Vector2) -> void:
|
||||
# Derived from the snapshot's live experience total rather than the roster's
|
||||
# copy, which only arrives when the set of characters changes.
|
||||
var level := Progression.level_for_xp(client.my_total_xp)
|
||||
var capped := level >= Progression.MAX_LEVEL
|
||||
var progress := Progression.level_progress(client.my_total_xp)
|
||||
var tint := COL_GOLD if capped else Color(0.6, 0.55, 1.0)
|
||||
_bar(at, progress, tint)
|
||||
# Brighter for a moment after a kill, so experience arriving is something
|
||||
# you notice rather than something you check.
|
||||
if _xp_flash > 0.0:
|
||||
tint = tint.lerp(Color.WHITE, 0.45 * clampf(_xp_flash, 0.0, 1.0))
|
||||
_bar(at, progress, tint, -1.0, Vector2(BAR_W, XP_H))
|
||||
# The number as well as the bar: "how far to the next level" is a question
|
||||
# a bar answers vaguely and a percentage answers exactly.
|
||||
var text := "MAX" if capped else "%d%% to level %d" % [
|
||||
int(floor(progress * 100.0)), level + 1]
|
||||
_text(at + Vector2(BAR_W + 10.0, 13.0), text, 12, tint)
|
||||
return true
|
||||
_text(at + Vector2(0.0, XP_H + 13.0), text, 11, Color(tint, 0.9))
|
||||
if _xp_flash > 0.0 and _xp_gain > 0:
|
||||
_text(at + Vector2(0.0, XP_H + 13.0), "+%d xp" % _xp_gain, 11,
|
||||
Color(COL_GOLD, clampf(_xp_flash, 0.0, 1.0)), HORIZONTAL_ALIGNMENT_RIGHT, BAR_W)
|
||||
|
||||
|
||||
## Unspent level-ups. Shown wherever you are, because the choice is banked in a
|
||||
@@ -332,12 +438,12 @@ func _draw_level_up_notice(y: float) -> void:
|
||||
if client.upgrades_pending <= 0:
|
||||
return
|
||||
var pulse := 0.5 + 0.5 * sin(float(Time.get_ticks_msec()) * 0.004)
|
||||
var where := " -- see the quartermaster in the hub" \
|
||||
var where := " -- quartermaster, in the hub" \
|
||||
if client.instance_kind != Protocol.InstanceKind.LOBBY else ""
|
||||
_text(Vector2(MARGIN, y + 16.0),
|
||||
_text(Vector2(MARGIN, y + 12.0),
|
||||
"%d LEVEL-UP%s TO SPEND%s" % [client.upgrades_pending,
|
||||
"" if client.upgrades_pending == 1 else "S", where],
|
||||
13, Color(COL_GOLD, 0.6 + 0.4 * pulse))
|
||||
12, Color(COL_GOLD, 0.6 + 0.4 * pulse))
|
||||
|
||||
|
||||
## The boss's health, once you have seen the boss.
|
||||
@@ -353,20 +459,12 @@ func _draw_boss_bar() -> void:
|
||||
var b := client.boss_state()
|
||||
if b.is_empty() or client.boss_def == null:
|
||||
return
|
||||
var seen_key := "%d:%d" % [client.instance_id, int(b["id"])]
|
||||
if _boss_seen != seen_key:
|
||||
if sight != null and sight.light_at(b["pos"]) <= 0.05:
|
||||
return
|
||||
_boss_seen = seen_key
|
||||
if not track_boss(b):
|
||||
return
|
||||
var pos := Vector2((_canvas.size.x - BOSS_BAR_W) * 0.5, MARGIN + 20.0)
|
||||
var frac := clampf(float(b["hp"]) / float(client.boss_def.max_hp), 0.0, 1.0)
|
||||
var rect := Rect2(pos, Vector2(BOSS_BAR_W, BAR_H))
|
||||
_hslice(UiTheme.texture("bar"), rect, Color.WHITE)
|
||||
var inner := Rect2(rect.position + Vector2(3.0, 3.0),
|
||||
Vector2((BOSS_BAR_W - 6.0) * frac, BAR_H - 6.0))
|
||||
if inner.size.x > 0.5:
|
||||
_canvas.draw_texture_rect(UiTheme.texture("bar_fill"), inner, false,
|
||||
Color(0.95, 0.35, 0.45))
|
||||
_panel(Rect2(pos.x - 10.0, MARGIN - 8.0, BOSS_BAR_W + 20.0, BAR_H + 36.0))
|
||||
_bar(pos, frac, Color(0.95, 0.35, 0.45), -1.0, Vector2(BOSS_BAR_W, BAR_H))
|
||||
var phase_index := int(b["phase"])
|
||||
var phase_name := ""
|
||||
if phase_index < client.boss_def.phases.size():
|
||||
@@ -377,16 +475,67 @@ func _draw_boss_bar() -> void:
|
||||
HORIZONTAL_ALIGNMENT_RIGHT, BOSS_BAR_W)
|
||||
|
||||
|
||||
## Arrival protection, as a banner rather than a line tucked beside the health
|
||||
## bar: it is the one moment the rules are different, and both halves of it --
|
||||
## you cannot be hurt, and you cannot shoot -- change what you should do.
|
||||
func _draw_arriving() -> void:
|
||||
if not client.my_spawn_grace:
|
||||
return
|
||||
## Whether the boss bar should be up, and the place a phase change is noticed.
|
||||
## Returns false until the fog has lit the boss once; true from then on, for the
|
||||
## rest of that fight.
|
||||
##
|
||||
## A phase change is announced because it is the moment the patterns you just
|
||||
## learned stop being the ones coming at you. Not the phase the boss is in when
|
||||
## first seen -- that is not a change, only where it started.
|
||||
func track_boss(b: Dictionary) -> bool:
|
||||
var seen_key := "%d:%d" % [client.instance_id, int(b["id"])]
|
||||
if _boss_seen != seen_key:
|
||||
if sight != null and sight.light_at(b["pos"]) <= 0.05:
|
||||
return false
|
||||
_boss_seen = seen_key
|
||||
var phase_index := int(b["phase"])
|
||||
if _boss_phase_key != seen_key:
|
||||
_boss_phase_key = seen_key
|
||||
_boss_phase = phase_index
|
||||
elif phase_index > _boss_phase:
|
||||
_boss_phase = phase_index
|
||||
var phase_name := ""
|
||||
if client.boss_def != null and phase_index < client.boss_def.phases.size():
|
||||
phase_name = client.boss_def.phases[phase_index].name
|
||||
banner(phase_name.to_upper(),
|
||||
client.boss_def.display_name if client.boss_def != null else "",
|
||||
Color(1.0, 0.55, 0.6))
|
||||
return true
|
||||
|
||||
|
||||
## One message across the middle of the screen, chosen by what matters most
|
||||
## right now: leaving, then arriving, then whatever was announced last.
|
||||
##
|
||||
## Arrival protection is a banner rather than a line beside the health bar
|
||||
## because it is the one moment the rules are different, and both halves of
|
||||
## it -- you cannot be hurt, and you cannot shoot -- change what you should do.
|
||||
func _draw_banner() -> void:
|
||||
var y := _canvas.size.y * 0.27
|
||||
var pulse := 0.5 + 0.5 * sin(float(Time.get_ticks_msec()) * 0.008)
|
||||
var y := _canvas.size.y * 0.28
|
||||
_centred(y, "ARRIVING", 24, Color(0.6, 0.9, 1.0, 0.75 + 0.25 * pulse))
|
||||
_centred(y + 22.0, "invulnerable · weapons cold", 13, Color(0.6, 0.9, 1.0, 0.8))
|
||||
if client.my_escaping:
|
||||
var blue := Color(0.55, 0.85, 1.0)
|
||||
_centred(y, "LEAVING", 24, blue)
|
||||
var w := 220.0
|
||||
var at := Vector2((_canvas.size.x - w) * 0.5, y + 12.0)
|
||||
_bar(at, client.my_escape, blue, -1.0, Vector2(w, 12.0))
|
||||
_centred(y + 42.0, "keep holding %s" % key("emergency_escape"), 12, Color(blue, 0.8))
|
||||
return
|
||||
if client.my_spawn_grace:
|
||||
_centred(y, "ARRIVING", 24, Color(0.6, 0.9, 1.0, 0.75 + 0.25 * pulse))
|
||||
_centred(y + 22.0, "invulnerable · weapons cold", 13, Color(0.6, 0.9, 1.0, 0.8))
|
||||
return
|
||||
if _banner_title.is_empty():
|
||||
return
|
||||
var age := _now() - _banner_born
|
||||
if age > BANNER_LIFE:
|
||||
return
|
||||
var alpha := clampf(minf(age / 0.15, (BANNER_LIFE - age) / BANNER_FADE), 0.0, 1.0)
|
||||
# A short settle on arrival rather than a pop, so it catches the eye without
|
||||
# jolting it away from the bullets.
|
||||
var settle := maxf(0.0, 0.2 - age) * 40.0
|
||||
_centred(y - settle, _banner_title, 28, Color(_banner_col, alpha))
|
||||
if not _banner_sub.is_empty():
|
||||
_centred(y + 24.0 - settle, _banner_sub, 13, Color(_banner_col, 0.8 * alpha))
|
||||
|
||||
|
||||
## Shown after the boss dies, so the victory lap has a visible clock on it.
|
||||
@@ -414,6 +563,7 @@ func _draw_others() -> void:
|
||||
func _draw_online() -> void:
|
||||
var x := _canvas.size.x - 240.0
|
||||
var y := MARGIN + 12.0
|
||||
_panel(Rect2(x - 10.0, MARGIN - 8.0, 236.0, 28.0 + 18.0 * client.roster.size()))
|
||||
_text(Vector2(x, y), "ONLINE", 13, COL_DIM)
|
||||
y += 20.0
|
||||
for entry in client.roster:
|
||||
@@ -444,6 +594,7 @@ func _draw_party() -> void:
|
||||
return
|
||||
var x := _canvas.size.x - 240.0
|
||||
var y := MARGIN + 12.0
|
||||
_panel(Rect2(x - 10.0, MARGIN - 8.0, 236.0, 26.0 + 20.0 * members.size()))
|
||||
_text(Vector2(x, y), "PARTY", 13, COL_DIM)
|
||||
y += 10.0
|
||||
var w := 140.0
|
||||
@@ -474,6 +625,47 @@ func _draw_party() -> void:
|
||||
y += 20.0
|
||||
|
||||
|
||||
## A marker at the edge of sight for each party member you cannot see, pointing
|
||||
## the way to them.
|
||||
##
|
||||
## The fog hides players behind walls and beyond sight like anything else, and
|
||||
## in a dungeon the most common question about the rest of your party is where
|
||||
## they went. Allies only, from data the client already holds -- a party member
|
||||
## outside the interest radius is not in the snapshot, and gets no marker.
|
||||
func _draw_party_markers() -> void:
|
||||
if client.instance_kind == Protocol.InstanceKind.LOBBY or not client.my_alive:
|
||||
return
|
||||
var eye := client.predicted_pos
|
||||
var centre := eye + client.camera_offset
|
||||
var ring := SimConfig.FOG_VIEW_RADIUS - 40.0
|
||||
var names := {}
|
||||
for entry in client.roster:
|
||||
names[int(entry["peer"])] = String(entry["name"])
|
||||
var bounds := Rect2(Vector2(MARGIN, MARGIN), _canvas.size - Vector2(MARGIN, MARGIN) * 2.0)
|
||||
for p in client.remote_players():
|
||||
var pos: Vector2 = p["pos"]
|
||||
var offset := pos - eye
|
||||
var dist := offset.length()
|
||||
if dist < 1.0:
|
||||
continue
|
||||
var lit := sight == null or sight.light_at(pos) > 0.3
|
||||
if dist < ring and lit:
|
||||
continue
|
||||
var dir := offset / dist
|
||||
var at := centre + dir * minf(ring, dist)
|
||||
at = at.clamp(bounds.position, bounds.end)
|
||||
var flags := int(p["flags"])
|
||||
var col: Color = p["colour"]
|
||||
if (flags & Protocol.F_ALIVE) == 0:
|
||||
col = COL_DANGER
|
||||
var tip := at + dir * 9.0
|
||||
var side := dir.orthogonal() * 6.0
|
||||
_canvas.draw_colored_polygon(PackedVector2Array([
|
||||
tip, at - dir * 4.0 + side, at - dir * 4.0 - side]), Color(col, 0.9))
|
||||
_text(at - dir * 12.0 + Vector2(-60.0, 4.0), String(names.get(int(p["peer"]), "")),
|
||||
11, Color(col, 0.85), HORIZONTAL_ALIGNMENT_CENTER, 120.0)
|
||||
|
||||
|
||||
## Four slots, always on screen. Deliberately not a panel you open: an
|
||||
## inventory you have to stop and read is a menu, and a menu is a death in a
|
||||
## game where the floor is bullets. Everything drawn here comes from the
|
||||
@@ -494,7 +686,11 @@ func _draw_inventory() -> void:
|
||||
# recoloured border, so "this is the one the key is on" reads the same
|
||||
# way every other pressed thing in the interface does.
|
||||
var slot_art := UiTheme.texture("slot_active" if i == held else "slot")
|
||||
_canvas.draw_texture_rect(slot_art, Rect2(at, Vector2(SLOT, SLOT)), false)
|
||||
# An empty slot steps back. The pack's slot is a bright parchment
|
||||
# square, and an empty one drew the eye as hard as a full one -- the
|
||||
# blank box was the most prominent thing in the row.
|
||||
var slot_tint := Color.WHITE if def != null else Color(1.0, 1.0, 1.0, 0.45)
|
||||
_canvas.draw_texture_rect(slot_art, Rect2(at, Vector2(SLOT, SLOT)), false, slot_tint)
|
||||
# The key that uses the slot, because that is the only thing the player
|
||||
# actually needs to know about it -- and it is whatever they bound.
|
||||
_canvas.draw_string(ThemeDB.fallback_font, at + Vector2(6.0, 17.0),
|
||||
@@ -613,14 +809,30 @@ func _draw_vignette() -> void:
|
||||
_canvas.draw_polygon(PackedVector2Array(sides[i]), PackedColorArray(cols[i]))
|
||||
|
||||
|
||||
## A bar in the pack's art: the frame stretched horizontally, the fill tinted.
|
||||
func _bar(pos: Vector2, frac: float, fill: Color) -> void:
|
||||
var rect := Rect2(pos, Vector2(BAR_W, BAR_H))
|
||||
## A bar in the pack's art: the frame stretched horizontally, a dark track
|
||||
## inside it, and the fill tinted on top.
|
||||
##
|
||||
## [param trail], when it is above [param frac], is drawn first in a pale
|
||||
## version of the fill -- the part of the bar the last hit took, before it
|
||||
## drains away. That is what makes a hit readable as an amount rather than as a
|
||||
## bar that is suddenly shorter.
|
||||
func _bar(pos: Vector2, frac: float, fill: Color, trail: float = -1.0,
|
||||
size: Vector2 = Vector2(BAR_W, BAR_H)) -> void:
|
||||
var rect := Rect2(pos, size)
|
||||
_hslice(UiTheme.texture("bar"), rect, Color.WHITE)
|
||||
var inset := 3.0
|
||||
var inner := Rect2(rect.position + Vector2(inset, inset),
|
||||
Vector2((rect.size.x - inset * 2.0) * clampf(frac, 0.0, 1.0),
|
||||
rect.size.y - inset * 2.0))
|
||||
# The frame scales with the bar's height, so the inset has to as well: a
|
||||
# fixed three pixels left a ten-pixel bar four pixels of track and six of
|
||||
# white frame, and it read as an empty white bar whatever it held.
|
||||
var inset := maxf(1.0, roundf(size.y * 0.15))
|
||||
var track := Rect2(rect.position + Vector2(inset, inset), rect.size - Vector2(inset, inset) * 2.0)
|
||||
_canvas.draw_rect(track, COL_TRACK)
|
||||
var full := track.size.x
|
||||
if trail > frac:
|
||||
# Darker than the fill, not paler. The health number sits on top of
|
||||
# this, and a pale trail was the one background it vanished against.
|
||||
_canvas.draw_rect(Rect2(track.position, Vector2(full * clampf(trail, 0.0, 1.0), track.size.y)),
|
||||
Color(fill.darkened(0.5), 0.95))
|
||||
var inner := Rect2(track.position, Vector2(full * clampf(frac, 0.0, 1.0), track.size.y))
|
||||
if inner.size.x > 0.5:
|
||||
# The fill sprite is a flat strip, so tinting it is the whole palette --
|
||||
# one texture covers health, experience, the escape channel and the
|
||||
|
||||
Reference in new issue
Block a user