Files
Cinderwild/scripts/ui/guide.gd

178 lines
6.1 KiB
GDScript

extends Control
## The tutorial's guide panel and marker (docs/TUTORIAL_DESIGN.md §1): what to
## do now, top right under the clock, with a progress count; a tick when a
## step's done; a pale glowing post where to go, and an arrow with the
## distance when it's off-screen. H hides the panel. Drawn from the Guide
## autoload; hidden when no tutorial is running.
const UITheme = preload("res://scripts/ui/theme.gd")
var player: Node3D
var _box: PanelContainer
var _chapter: Label
var _title: Label
var _text: Label
var _progress: Label
var _tick: Label
var _arrow: Label
var _marker: Node3D
var _hidden := false
func setup(p: Node3D) -> void:
player = p
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_IGNORE
_box = UITheme.panel()
_box.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
_box.grow_horizontal = Control.GROW_DIRECTION_BEGIN
_box.offset_right = -22
_box.offset_top = 48
_box.custom_minimum_size = Vector2(340, 0)
_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_box)
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 4)
v.custom_minimum_size = Vector2(320, 0)
_box.add_child(v)
_chapter = UITheme.label("", 13, UITheme.TEXT_DIM)
v.add_child(_chapter)
_title = UITheme.label("", 19)
v.add_child(_title)
_text = UITheme.label("", 15, UITheme.TEXT_DIM)
_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_text.custom_minimum_size = Vector2(320, 0)
v.add_child(_text)
_progress = UITheme.label("", 15, Color(0.95, 0.82, 0.5))
v.add_child(_progress)
_tick = UITheme.label("", 22, Color(0.55, 0.9, 0.45))
_tick.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
_tick.offset_top = 90
_tick.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_tick.modulate.a = 0.0
add_child(_tick)
_arrow = UITheme.label("", 20, Color(0.75, 0.95, 1.0))
_arrow.visible = false
add_child(_arrow)
Guide.step_started.connect(func(_i, _s): _redraw())
Guide.progressed.connect(_redraw)
Guide.step_done.connect(_on_done)
Guide.started.connect(_redraw)
Guide.finished.connect(_on_finished)
_redraw()
func _unhandled_input(event: InputEvent) -> void:
if Guide.active and event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_H:
_hidden = not _hidden
_redraw()
func _redraw() -> void:
var s: Dictionary = Guide.current()
visible = Guide.active
_box.visible = Guide.active and not _hidden and not s.is_empty()
if s.is_empty():
return
_chapter.text = "%s · step %d of %d (H hides)" % [Guide.chapter_title(s), Guide.index + 1, Guide.steps.size()]
_title.text = String(s.title)
# The hermit's words, in quotes, with his name.
_text.text = ("%s: \"%s\"" % [Guide.guide_name, String(s.text)]) if not Guide.guide_name.is_empty() else String(s.text)
_progress.text = Guide.progress_text()
func _on_done(_i: int, s: Dictionary) -> void:
_tick.text = "✓ " + String(s.title)
var tw := create_tween()
_tick.modulate.a = 1.0
tw.tween_interval(1.4)
tw.tween_property(_tick, "modulate:a", 0.0, 0.6)
func _on_finished() -> void:
_box.visible = false
_tick.text = "✓ Chapter done. More of the tutorial is on its way."
_tick.modulate.a = 1.0
var tw := create_tween()
tw.tween_interval(3.0)
tw.tween_property(_tick, "modulate:a", 0.0, 1.0)
func _process(_delta: float) -> void:
_update_marker()
## The glowing post at the target, and the arrow at the screen edge.
func _update_marker() -> void:
var at: Vector3 = Guide.marker_position() if Guide.active else Vector3.INF
if _marker != null and (not is_instance_valid(_marker) or _marker.get_parent() != Guide.world):
if is_instance_valid(_marker):
_marker.queue_free()
_marker = null # (its world went: a reload, or back to the title)
if _marker == null and Guide.active and Guide.world != null:
_marker = make_marker()
Guide.world.add_child(_marker)
if _marker == null:
_arrow.visible = false
return
_marker.visible = at != Vector3.INF
if at == Vector3.INF:
_arrow.visible = false
return
_marker.global_position = at
var cam := get_viewport().get_camera_3d()
if cam == null or _hidden:
_arrow.visible = false
return
var vis := not cam.is_position_behind(at)
var sp := cam.unproject_position(at + Vector3(0, 1.0, 0))
var r := get_viewport_rect()
if vis and r.grow(-40).has_point(sp):
_arrow.visible = false
return
# Off-screen: an arrow at the edge, pointing the way, with the distance.
var c := r.size * 0.5
var dir := (sp - c) * (-1.0 if not vis else 1.0)
if dir.length() < 1.0:
dir = Vector2(0, -1)
dir = dir.normalized()
var edge := c + dir * (minf(c.x, c.y) - 70.0)
edge = edge.clamp(Vector2(40, 40), r.size - Vector2(140, 60))
var arrows := ["→", "↘", "↓", "↙", "←", "↖", "↑", "↗"]
var k := wrapi(roundi(atan2(dir.y, dir.x) / (PI / 4.0)), 0, 8)
var who: Node3D = Guide.world.player if Guide.world != null else player
_arrow.text = "%s %d m" % [arrows[k], roundi(who.global_position.distance_to(at))]
_arrow.position = edge
_arrow.visible = true
## A pale, glowing post with a ring at its foot.
static func make_marker() -> Node3D:
var root := Node3D.new()
root.name = "GuideMarker"
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
m.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
m.albedo_color = Color(0.55, 0.85, 1.0, 0.35)
m.cull_mode = BaseMaterial3D.CULL_DISABLED
var beam := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = 0.06
cyl.bottom_radius = 0.14
cyl.height = 5.0
cyl.material = m
beam.mesh = cyl
beam.position = Vector3(0, 2.6, 0)
beam.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
root.add_child(beam)
var ring := MeshInstance3D.new()
var torus := TorusMesh.new()
torus.inner_radius = 0.55
torus.outer_radius = 0.7
torus.material = m
ring.mesh = torus
ring.position = Vector3(0, 0.05, 0)
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
root.add_child(ring)
if DisplayServer.get_name() != "headless":
var l := OmniLight3D.new()
l.light_color = Color(0.55, 0.85, 1.0)
l.light_energy = 0.6
l.omni_range = 4.0
l.position = Vector3(0, 1.0, 0)
root.add_child(l)
return root