Implement speech style toolbar live-edit refinements and whole speech show controls

- Added a new specification for the speech style toolbar live-edit, detailing immediate style application, scoped effect demonstration, and hover feedback.
- Introduced refinements to the speech style toolbar, including improved chooser sizing, exclusive stage editing during speech composition, and removal of redundant controls.
- Documented new stage controls for managing how the whole speech shows, including options for bubble arrangement, word appearance, and bubble motion.
- Created a new log file for speech padding syntax to track engine versioning.
This commit is contained in:
2026-10-03 14:49:22 -04:00
parent 6a1359f244
commit 6666173c4f
42 changed files with 9163 additions and 165 deletions
+42 -20
View File
@@ -6,7 +6,7 @@ extends Node
const ACTION_REGISTRY := preload("res://scripts/action_registry.gd")
const SPEECH_BUBBLE_SCRIPT := preload("res://scripts/stickman_speech_bubble.gd")
const SPEECH_BUBBLE_OFFSET := Vector2(0.0, -640.0)
const SPEECH_PAYLOAD := preload("res://scripts/speech_payload.gd")
enum RunnerState { IDLE, EXECUTING }
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING, PROP_INTERACTION }
@@ -40,6 +40,7 @@ var _phase_timer: float = 0.0
var _primary_speech: Dictionary = {}
var _overlay_speech: Dictionary = {}
var _speech_bubble: Node2D = null
var _speech_preview_suppressed: bool = false
var _action_ids: Array[int] = []
var _next_action_id: int = 1
var _current_action_id: int = -1
@@ -90,15 +91,33 @@ func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
func speak(text: String, duration: float) -> void:
_primary_speech = {"text": text, "time_left": maxf(duration, 0.0)}
speak_action({"type": "speak", "text": text, "duration": maxf(duration, 0.0)})
func speak_action(action: Dictionary) -> void:
var normalized: Dictionary = SPEECH_PAYLOAD.normalize(action)
var duration: float = SPEECH_PAYLOAD.duration_for(normalized)
_primary_speech = {"action": normalized, "elapsed": 0.0, "duration": duration}
_show_visible_speech()
func speak_overlay(text: String, duration: float) -> void:
_overlay_speech = {"text": text, "time_left": maxf(duration, 0.0)}
speak_overlay_action({"type": "speak", "text": text, "duration": maxf(duration, 0.0)})
func speak_overlay_action(action: Dictionary) -> void:
var normalized: Dictionary = SPEECH_PAYLOAD.normalize(action)
var duration: float = SPEECH_PAYLOAD.duration_for(normalized)
_overlay_speech = {"action": normalized, "elapsed": 0.0, "duration": duration}
_show_visible_speech()
func set_speech_preview_suppressed(value: bool) -> void:
_speech_preview_suppressed = value
if _speech_bubble != null and is_instance_valid(_speech_bubble):
(_speech_bubble as SpeechBubble).set_suppressed(value)
func hide_speech() -> void:
_primary_speech.clear()
_overlay_speech.clear()
@@ -108,7 +127,7 @@ func hide_speech() -> void:
func sync_facing(profile: int) -> void:
if _speech_bubble != null and is_instance_valid(_speech_bubble):
_speech_bubble.scale = Vector2(-1.0, 1.0) if profile == StickmanRig.FacingProfile.LEFT else Vector2.ONE
(_speech_bubble as SpeechBubble).set_mirrored(profile == StickmanRig.FacingProfile.LEFT)
func set_authored_queue(actions: Array[Dictionary]) -> bool:
@@ -266,7 +285,7 @@ func enqueue_reactive(actions: Array[Dictionary]) -> void:
for action: Dictionary in actions:
var role: int = ACTION_REGISTRY.reactive_role(action)
if role == ACTION_REGISTRY.ReactiveRole.OVERLAY:
speak_overlay(String(action.get("text", "")), float(action.get("duration", 2.0)))
speak_overlay_action(action)
continue
if role == ACTION_REGISTRY.ReactiveRole.TAKEOVER:
if not takeover_seen:
@@ -394,8 +413,12 @@ func _begin_action(action: Dictionary) -> void:
if _locomotion == null or not _locomotion.walk_to(action.get("target", Vector2.ZERO), float(action.get("speed", -1.0))):
_finish_action()
"speak":
_action_phase = ActionPhase.SPEAKING
speak(String(action.get("text", "")), float(action.get("duration", 2.0)))
if bool(action.get("keep_acting", false)):
speak_overlay_action(action)
_finish_action()
else:
_action_phase = ActionPhase.SPEAKING
speak_action(action)
"wait":
_phase_timer = float(action.get("duration", 0.0))
_action_phase = ActionPhase.WAITING
@@ -523,22 +546,20 @@ func _is_blocked() -> bool:
func _update_speech(delta: float) -> void:
var visible_owner_changed: bool = false
if not _primary_speech.is_empty() and not _is_blocked():
_primary_speech["time_left"] = float(_primary_speech.get("time_left", 0.0)) - delta
if float(_primary_speech["time_left"]) <= 0.0:
_primary_speech.clear()
speech_finished.emit()
if not _overlay_speech.is_empty():
_overlay_speech["time_left"] = float(_overlay_speech.get("time_left", 0.0)) - delta
if float(_overlay_speech["time_left"]) <= 0.0:
_overlay_speech["elapsed"] = float(_overlay_speech.get("elapsed", 0.0)) + delta
if float(_overlay_speech.get("elapsed", 0.0)) >= float(_overlay_speech.get("duration", 0.0)):
_overlay_speech.clear()
visible_owner_changed = true
speech_finished.emit()
elif not _primary_speech.is_empty() and not _is_blocked():
_primary_speech["elapsed"] = float(_primary_speech.get("elapsed", 0.0)) + delta
if float(_primary_speech.get("elapsed", 0.0)) >= float(_primary_speech.get("duration", 0.0)):
_primary_speech.clear()
speech_finished.emit()
if _overlay_speech.is_empty() and _primary_speech.is_empty():
if _speech_bubble != null and is_instance_valid(_speech_bubble):
_speech_bubble.hide_bubble()
elif visible_owner_changed and _overlay_speech.is_empty():
else:
_show_visible_speech()
@@ -546,11 +567,12 @@ func _show_visible_speech() -> void:
if _rig == null:
return
if _speech_bubble == null or not is_instance_valid(_speech_bubble):
_speech_bubble = SPEECH_BUBBLE_SCRIPT.new() as Node2D
_speech_bubble = SPEECH_BUBBLE_SCRIPT.new() as SpeechBubble
_speech_bubble.name = "SpeechBubble"
_speech_bubble.position = SPEECH_BUBBLE_OFFSET
_rig.add_child(_speech_bubble)
sync_facing(_rig.facing_profile)
(_speech_bubble as SpeechBubble).set_suppressed(_speech_preview_suppressed)
var visible_speech: Dictionary = _overlay_speech if not _overlay_speech.is_empty() else _primary_speech
if not visible_speech.is_empty():
_speech_bubble.show_text(String(visible_speech.get("text", "")))
var action: Dictionary = visible_speech.get("action", {})
(_speech_bubble as SpeechBubble).render_action(action, float(visible_speech.get("elapsed", 0.0)), false)