239 lines
7.6 KiB
GDScript
239 lines
7.6 KiB
GDScript
class_name TutorialMascot
|
|
extends Control
|
|
## Маскот «БАЙТ» — дружелюбный робот-дискета 96×96, нарисованный кодом.
|
|
## Умеет показывать облачко с текстом и номер шага туториала.
|
|
##
|
|
## mascot.show_message("Привет! Я БАЙТ. Начнём со смены?", 4.0)
|
|
## mascot.set_step(1)
|
|
## mascot.hide_message()
|
|
##
|
|
## Контрол не блокирует мышь. Высота подстраивается под облачко; робот
|
|
## прижат к нижнему левому углу, поэтому в игре его удобно ставить у края.
|
|
|
|
signal message_finished
|
|
|
|
const ROBOT := 96.0
|
|
const GAP := 10.0
|
|
const BUBBLE_W := 244.0
|
|
|
|
var is_visible := false
|
|
var step := 0
|
|
var bubble_width := BUBBLE_W
|
|
## Масштаб только робота (облачко остаётся читаемым). 1.0 — базовые 96 px.
|
|
var robot_scale := 1.0
|
|
|
|
var _text := ""
|
|
var _lines := PackedStringArray()
|
|
var _auto_hide := -1.0
|
|
var _timer := 0.0
|
|
var _t := 0.0
|
|
var _appear := 0.0
|
|
var _blink_t := 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
focus_mode = Control.FOCUS_NONE
|
|
custom_minimum_size = _robot_size()
|
|
size = _robot_size()
|
|
visible = false
|
|
set_process(false)
|
|
|
|
|
|
func _robot_size() -> Vector2:
|
|
var s := ROBOT * robot_scale
|
|
return Vector2(s, s)
|
|
|
|
|
|
## Уменьшить/увеличить только робота, не трогая кегль облачка.
|
|
func set_robot_scale(value: float) -> void:
|
|
robot_scale = clampf(value, 0.5, 2.0)
|
|
custom_minimum_size = _robot_size()
|
|
if not _text.is_empty():
|
|
_relayout()
|
|
else:
|
|
size = _robot_size()
|
|
queue_redraw()
|
|
|
|
|
|
## Показать облачко. seconds > 0 — спрятать автоматически.
|
|
func show_message(text: String, seconds: float = -1.0) -> void:
|
|
_text = text
|
|
is_visible = true
|
|
visible = true
|
|
_auto_hide = seconds
|
|
_timer = 0.0
|
|
_appear = 0.0
|
|
_relayout()
|
|
set_process(true)
|
|
queue_redraw()
|
|
|
|
|
|
## Спрятать маскота целиком вместе с облачком.
|
|
func hide_message() -> void:
|
|
is_visible = false
|
|
visible = false
|
|
_text = ""
|
|
_lines = PackedStringArray()
|
|
_auto_hide = -1.0
|
|
_timer = 0.0
|
|
_appear = 0.0
|
|
size = _robot_size()
|
|
custom_minimum_size = size
|
|
set_process(false)
|
|
queue_redraw()
|
|
|
|
|
|
## Номер шага: 0 — без шага; 1..N показывается чипом в облачке.
|
|
func set_step(index: int) -> void:
|
|
step = maxi(0, index)
|
|
if is_visible and not _text.is_empty():
|
|
_relayout()
|
|
queue_redraw()
|
|
|
|
|
|
func _relayout() -> void:
|
|
var f := Theme90.ui()
|
|
_lines = Theme90.wrap(f, _text, 12, bubble_width - 26.0, 4)
|
|
var bh := _bubble_height()
|
|
var rs := _robot_size()
|
|
var total := maxf(rs.y, bh)
|
|
size = Vector2(rs.x + GAP + bubble_width, total)
|
|
custom_minimum_size = size
|
|
|
|
|
|
func _bubble_height() -> float:
|
|
if _text.is_empty():
|
|
return 0.0
|
|
var pad_top := 25.0 if step > 0 else 12.0
|
|
return maxf(56.0, pad_top + _lines.size() * 14.0 + 12.0)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
_t += delta
|
|
_blink_t += delta
|
|
if _blink_t > 3.4:
|
|
_blink_t = 0.0
|
|
if _appear < 1.0:
|
|
_appear = minf(1.0, _appear + delta * 5.5)
|
|
if _auto_hide > 0.0:
|
|
_timer += delta
|
|
if _timer >= _auto_hide:
|
|
hide_message()
|
|
message_finished.emit()
|
|
return
|
|
queue_redraw()
|
|
|
|
|
|
func _draw() -> void:
|
|
var bob := sin(_t * 2.1) * 1.1
|
|
var rs := _robot_size()
|
|
var ry := size.y - rs.y + bob
|
|
_draw_robot(ry)
|
|
if not _text.is_empty() and _appear > 0.0:
|
|
_draw_bubble(ry)
|
|
|
|
|
|
func _draw_robot(ry: float) -> void:
|
|
var x := 0.0
|
|
var y := roundf(ry)
|
|
var outline := Color(Theme90.NAVY.r, Theme90.NAVY.g, Theme90.NAVY.b, 0.55)
|
|
var rs := robot_scale
|
|
draw_set_transform(Vector2(0, y - y * rs), 0.0, Vector2(rs, rs))
|
|
|
|
# тень
|
|
draw_style_box(Theme90.fill_box(Color(0, 0, 0, 0.07), 5),
|
|
Rect2(x + 17, y + 87, 62, 7))
|
|
|
|
# антенна
|
|
draw_line(Vector2(x + 48, y + 15), Vector2(x + 48, y + 7), outline, 1.4)
|
|
draw_circle(Vector2(x + 48, y + 5), 3.0, Theme90.ACCENT)
|
|
|
|
# корпус
|
|
draw_style_box(Theme90.box(Theme90.PAPER_HI, outline, 11, 1, Theme90.SHADOW),
|
|
Rect2(x + 12, y + 14, 72, 72))
|
|
|
|
# щель дискеты
|
|
draw_style_box(Theme90.fill_box(Theme90.NAVY_DEEP, 2),
|
|
Rect2(x + 36, y + 18, 24, 7))
|
|
draw_rect(Rect2(x + 49, y + 19, 2, 5), Color(1, 1, 1, 0.35))
|
|
|
|
# экран-лицо
|
|
draw_style_box(Theme90.box(Theme90.ACCENT_PALE, Theme90.BORDER_SOFT, 7, 1),
|
|
Rect2(x + 24, y + 31, 48, 30))
|
|
# глаза с редким морганием
|
|
var blink := _blink_t > 3.22 and _blink_t < 3.34
|
|
if blink:
|
|
draw_rect(Rect2(x + 39, y + 47, 5, 1.4), Theme90.NAVY)
|
|
draw_rect(Rect2(x + 53, y + 47, 5, 1.4), Theme90.NAVY)
|
|
else:
|
|
draw_style_box(Theme90.fill_box(Theme90.NAVY, 2),
|
|
Rect2(x + 39, y + 42, 5, 7))
|
|
draw_style_box(Theme90.fill_box(Theme90.NAVY, 2),
|
|
Rect2(x + 53, y + 42, 5, 7))
|
|
draw_circle(Vector2(x + 40.5, y + 44), 1.0, Color(1, 1, 1, 0.8))
|
|
draw_circle(Vector2(x + 54.5, y + 44), 1.0, Color(1, 1, 1, 0.8))
|
|
# улыбка
|
|
draw_arc(Vector2(x + 48, y + 51), 6.0, 0.20 * PI, 0.80 * PI, 10, Theme90.NAVY, 1.8)
|
|
# щёки
|
|
draw_circle(Vector2(x + 31, y + 55), 2.4, Color(Theme90.ERROR.r, Theme90.ERROR.g,
|
|
Theme90.ERROR.b, 0.18))
|
|
draw_circle(Vector2(x + 65, y + 55), 2.4, Color(Theme90.ERROR.r, Theme90.ERROR.g,
|
|
Theme90.ERROR.b, 0.18))
|
|
|
|
# подпись на корпусе
|
|
var f := Theme90.ui()
|
|
var label := "БАЙТ"
|
|
var lw := f.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, 9).x
|
|
Theme90.text(self, f, Vector2(x + 48 - lw * 0.5, y + 66), label, 9, Theme90.NAVY)
|
|
if step > 0 and _text.is_empty():
|
|
draw_circle(Vector2(x + 72, y + 76), 6.0, Theme90.ACCENT)
|
|
var sn := str(step)
|
|
var sw := f.get_string_size(sn, HORIZONTAL_ALIGNMENT_LEFT, -1, 9).x
|
|
Theme90.text(self, f, Vector2(x + 72 - sw * 0.5, y + 71), sn, 9, Theme90.WHITE)
|
|
|
|
# руки и ноги
|
|
draw_style_box(Theme90.box(Theme90.ACCENT_LIGHT, Color(0, 0, 0, 0), 4, 0),
|
|
Rect2(x + 3, y + 42, 9, 22))
|
|
draw_style_box(Theme90.box(Theme90.ACCENT_LIGHT, Color(0, 0, 0, 0), 4, 0),
|
|
Rect2(x + 84, y + 42, 9, 22))
|
|
draw_style_box(Theme90.fill_box(Theme90.NAVY, 3), Rect2(x + 23, y + 86, 18, 7))
|
|
draw_style_box(Theme90.fill_box(Theme90.NAVY, 3), Rect2(x + 55, y + 86, 18, 7))
|
|
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
|
|
|
|
|
func _draw_bubble(ry: float) -> void:
|
|
var a := _appear
|
|
var bh := _bubble_height()
|
|
var bx := _robot_size().x + GAP
|
|
var by := roundf((size.y - bh) * 0.5)
|
|
var rect := Rect2(bx, by, bubble_width, bh)
|
|
var f := Theme90.ui()
|
|
|
|
# хвостик к роботу
|
|
var tail_y := by + bh * 0.5
|
|
draw_colored_polygon(PackedVector2Array([
|
|
Vector2(bx + 2, tail_y - 7), Vector2(bx - 8, tail_y), Vector2(bx + 2, tail_y + 7)]),
|
|
Color(1, 1, 1, a))
|
|
draw_line(Vector2(bx + 2, tail_y - 7), Vector2(bx - 8, tail_y),
|
|
Color(Theme90.BORDER.r, Theme90.BORDER.g, Theme90.BORDER.b, a * 0.9), 1.0)
|
|
draw_line(Vector2(bx - 8, tail_y), Vector2(bx + 2, tail_y + 7),
|
|
Color(Theme90.BORDER.r, Theme90.BORDER.g, Theme90.BORDER.b, a * 0.9), 1.0)
|
|
|
|
draw_style_box(Theme90.box(Color(1, 1, 1, a),
|
|
Color(Theme90.BORDER.r, Theme90.BORDER.g, Theme90.BORDER.b, a), 8, 1,
|
|
Color(0, 0, 0, 0.08 * a)), rect)
|
|
|
|
var ty := by + (25.0 if step > 0 else 12.0)
|
|
if step > 0:
|
|
var chip := Rect2(bx + 12, by + 8, 40, 14)
|
|
draw_style_box(Theme90.box(Color(Theme90.ACCENT_PALE.r, Theme90.ACCENT_PALE.g,
|
|
Theme90.ACCENT_PALE.b, a), Color(0, 0, 0, 0), 3, 0), chip)
|
|
Theme90.text(self, f, Vector2(chip.position.x + 7, chip.position.y + 2),
|
|
"ШАГ %d" % step, 9, Theme90.NAVY, -1, HORIZONTAL_ALIGNMENT_LEFT)
|
|
|
|
var ink := Color(Theme90.INK.r, Theme90.INK.g, Theme90.INK.b, a)
|
|
for i in _lines.size():
|
|
Theme90.text(self, f, Vector2(bx + 13, ty + i * 14), _lines[i], 12, ink,
|
|
bubble_width - 26.0)
|