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)