diff --git a/ui/component/DialogueTextbox.gd b/ui/component/DialogueTextbox.gd index caff224..612e888 100644 --- a/ui/component/DialogueTextbox.gd +++ b/ui/component/DialogueTextbox.gd @@ -22,6 +22,7 @@ var _linesPerPage:int = 0 var _revealTimer:float = 0.0 var _pauseTimer:float = 0.0 var _autoAdvanceTimer:float = 0.0 +var _layoutReady:bool = false var _isRevealing:bool = false var _isWaitingForInput:bool = false var _hasLetGoOfInteract:bool = true @@ -44,20 +45,23 @@ func setup(line:DialogueLine, entity:Entity, mode:AdvancementMode = AdvancementM _advancementMode = mode _speakerLabel.text = entity.displayName if entity else "" _speakerLabel.visible = _speakerLabel.text != "" + # Show all characters so Godot shapes the full text and computes line-break + # positions before reveal starts. The box is transparent this frame. _bodyLabel.text = line.text - _bodyLabel.visible_characters = 0 + _bodyLabel.visible_characters = -1 _bodyLabel.scroll_to_line(0) _startLine = 0 _linesPerPage = LINES_PER_PAGE _revealTimer = 0.0 _pauseTimer = 0.0 _autoAdvanceTimer = 0.0 + _layoutReady = false + _isRevealing = false _isWaitingForInput = false _hasLetGoOfInteract = !Input.is_action_pressed("interact") size.x = MAX_WIDTH + modulate.a = 0.0 visible = true - _parsedText = _bodyLabel.get_parsed_text() - _isRevealing = true func _process(delta:float) -> void: if not visible: @@ -68,6 +72,10 @@ func _process(delta:float) -> void: if _isWaitingForInput: _advanceIndicator.modulate.a = 0.5 + 0.5 * sin(Time.get_ticks_msec() / 300.0) + if not _layoutReady: + _finishLayout() + return + if _isRevealing: _processReveal(delta) return @@ -79,6 +87,40 @@ func _process(delta:float) -> void: if _advancementMode == AdvancementMode.TIMED: _processAutoAdvance(delta) +# Called on the first _process frame after setup(). By this point Godot has +# shaped the full text, so get_character_line() returns stable positions. +# We bake explicit \n characters into the text at every wrap boundary, then +# disable autowrap so the layout never changes during reveal. +func _finishLayout() -> void: + var raw:String = _bodyLabel.get_parsed_text() + _parsedText = _buildPreWrappedText(raw) + _bodyLabel.text = _parsedText + _bodyLabel.autowrap_mode = TextServer.AUTOWRAP_OFF + _bodyLabel.visible_characters = 0 + modulate.a = 1.0 + _layoutReady = true + _isRevealing = true + +func _buildPreWrappedText(parsed:String) -> String: + if parsed.is_empty(): + return parsed + var result:String = "" + var prevLine:int = 0 + for i in range(len(parsed)): + var ch:String = parsed[i] + # Source-level newlines advance the expected line counter directly; + # skip the charLine check so we never double-insert. + if ch == "\n": + result += "\n" + prevLine += 1 + continue + var charLine:int = _bodyLabel.get_character_line(i) + if charLine > prevLine: + result += "\n" + prevLine = charLine + result += ch + return result + func _processReveal(delta:float) -> void: if _pauseTimer > 0.0: var speedMult:float = SPEEDUP_MULTIPLIER if Input.is_action_pressed("interact") else 1.0 @@ -102,16 +144,20 @@ func _revealNextChar() -> bool: _bodyLabel.visible_characters += 1 var idx:int = _bodyLabel.visible_characters - 1 - # Stop if this character has wrapped onto the next page. - if idx > 0: - var charLine:int = _bodyLabel.get_character_line(idx) - if charLine >= _startLine + _linesPerPage: - _bodyLabel.visible_characters -= 1 - _onPageFull() - return false + # Count newlines up to idx to get the current line — no Godot layout call needed. + var charLine:int = _parsedText.left(idx + 1).count("\n") + if charLine >= _startLine + _linesPerPage: + _bodyLabel.visible_characters -= 1 + _onPageFull() + return false if idx < len(_parsedText): - _pauseTimer = _getPauseForChar(idx) + var isLastVisible:bool = idx >= len(_parsedText) - 1 + if not isLastVisible: + var nextCharLine:int = _parsedText.left(idx + 2).count("\n") + isLastVisible = nextCharLine >= _startLine + _linesPerPage + if not isLastVisible: + _pauseTimer = _getPauseForChar(idx) return true @@ -157,7 +203,8 @@ func _processAutoAdvance(delta:float) -> void: func _advance() -> void: _advanceIndicator.visible = false _advanceIndicator.modulate.a = 1.0 - var hasMorePages:bool = _startLine + _linesPerPage < _bodyLabel.get_line_count() + var totalLines:int = _parsedText.count("\n") + 1 + var hasMorePages:bool = _startLine + _linesPerPage < totalLines if hasMorePages: _startLine += _linesPerPage _bodyLabel.scroll_to_line(_startLine)