This commit is contained in:
2026-03-09 11:57:48 -07:00
parent 041c0ebbdb
commit 7203c843ec
10 changed files with 246 additions and 21 deletions

View File

@@ -72,6 +72,19 @@ font_size = 32
outline_size = 5
outline_color = Color(0, 0, 0, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_narrator"]
bg_color = Color(0, 0, 0, 0.75)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="LabelSettings" id="LabelSettings_narrator"]
font = ExtResource("5_0olt8")
font_size = 28
outline_size = 4
outline_color = Color(0, 0, 0, 1)
[node name="Node2D" type="Node2D" unique_id=826166852]
script = ExtResource("2")
@@ -136,3 +149,36 @@ script = ExtResource("5_rglkg")
[node name="dialogue" parent="." unique_id=186154081 instance=ExtResource("7_fj12q")]
position = Vector2(328, 106)
scale = Vector2(0.75, 0.75)
[node name="NarratorOverlay" type="CanvasLayer" parent="." unique_id=987654321]
layer = 10
[node name="Panel" type="Panel" parent="NarratorOverlay" unique_id=987654322]
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -600.0
offset_top = -120.0
offset_right = 600.0
offset_bottom = -20.0
grow_horizontal = 2
grow_vertical = 0
theme_override_styles/panel = SubResource("StyleBoxFlat_narrator")
[node name="Label" type="Label" parent="NarratorOverlay/Panel" unique_id=987654323]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 15.0
offset_right = -20.0
offset_bottom = -15.0
grow_horizontal = 2
grow_vertical = 2
label_settings = SubResource("LabelSettings_narrator")
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 3

View File

@@ -21,6 +21,9 @@ class ScriptNode:
func interrupt():
return false
func handle_input(event: InputEvent) -> bool:
return false
class Say:
extends ScriptNode
var text
@@ -73,6 +76,96 @@ class Say:
subject.find_child("label-root").hide()
class Narrate:
extends ScriptNode
var text
var done = false
var started = false
var dismissed_by_click = false
var fade_in_duration = 0.3
var fade_out_duration = 0.3
var chars_per_second = 20.0 # Reading speed
var min_duration = 2.0
var max_duration = 10.0
func step_type():
return "Narrate " + text.substr(0, 20)
func calculate_duration() -> float:
var duration = text.length() / chars_per_second
return clamp(duration, min_duration, max_duration)
func do(delta):
if !started:
started = true
dismissed_by_click = false
# Show narrator overlay with fade in
var narrator_overlay = scene.find_child("NarratorOverlay")
if narrator_overlay:
var panel = narrator_overlay.find_child("Panel")
var label = panel.find_child("Label")
if label:
label.text = text
if panel:
panel.modulate = Color(1, 1, 1, 0)
panel.show()
# Fade in
var tween = scene.create_tween()
tween.tween_property(panel, "modulate", Color(1, 1, 1, 1), fade_in_duration)
await tween.finished
# Set global flag that narrator is active
scene.set_meta("narrator_active", true)
# Calculate display duration
var display_duration = calculate_duration()
# Wait for either timeout or click
var timer = scene.get_tree().create_timer(display_duration)
while timer.time_left > 0 and not dismissed_by_click:
await scene.get_tree().process_frame
# Fade out (keep narrator_active flag true during fade to prevent walk trigger)
var overlay = scene.find_child("NarratorOverlay")
if overlay:
var p = overlay.find_child("Panel")
if p and p.visible:
var tween = scene.create_tween()
tween.tween_property(p, "modulate", Color(1, 1, 1, 0), fade_out_duration)
await tween.finished
p.hide()
# Clear global flag AFTER fade completes
scene.set_meta("narrator_active", false)
done = true
started = true
func is_done():
return done
func dismiss():
dismissed_by_click = true
func interrupt():
done = true
var overlay = scene.find_child("NarratorOverlay")
if overlay:
var p = overlay.find_child("Panel")
if p:
p.hide()
scene.set_meta("narrator_active", false)
func handle_input(event: InputEvent) -> bool:
if event is InputEventMouseButton and Input.is_action_just_released("interact"):
if not dismissed_by_click and not done:
dismissed_by_click = true
return true
return false
class WalkTo:
extends ScriptNode
var path = []
@@ -293,6 +386,12 @@ class ScriptGraph:
c.interrupt()
return can_interrupt
func handle_input(event: InputEvent) -> bool:
for c in current:
if c.handle_input(event):
return true
return false
var script_graph: ScriptGraph
#var last_node: ScriptNode
@@ -340,6 +439,19 @@ func say(subject, text, stop=true):
say.stop = stop
return say
func narrate(text):
var narrate = Narrate.new()
narrate.text = text
return narrate
func dismiss_current_narrator():
if current_script:
for node in current_script.current:
if node is Narrate:
node.dismiss()
return true
return false
func walk_to_deferred(named_from, named_to):
var say = WalkToDeferred.new()
say.subject_name = "SceneViewport/background/Graham"

View File

@@ -1,11 +1,11 @@
extends Node2D
# Declare member variables here. Examples:
# var a = 2
# var b = "text"
var cursors = [load("res://boot_icon.png"), load("res://eye_icon.png"), load("res://hand_icon.png"), load("res://speech_icon.png")]
var cursor_index = 0
var hourglass_cursor = load("res://hourglass_icon.png")
var previous_cursor_index: int = 0
var is_script_running: bool = false
var is_cursor_locked: bool = false # When true, hourglass is shown and cursor can't be changed
func get_scene() -> Scene:
return $SceneViewport/background
@@ -51,12 +51,30 @@ func _process(delta):
var s = get_scene().ego_scale(player)
player.scale = Vector2(s, s)
# Update cursor if script state changed
var script = $GameScript.current_script
if script and not is_script_running:
set_script_cursor()
elif not script and is_script_running:
restore_cursor()
func set_script_cursor() -> void:
previous_cursor_index = ActionState.current_action
is_script_running = true
is_cursor_locked = true # Lock cursor to hourglass
Input.set_custom_mouse_cursor(hourglass_cursor)
func restore_cursor() -> void:
is_script_running = false
is_cursor_locked = false # Unlock cursor
Input.set_custom_mouse_cursor(cursors[previous_cursor_index])
func _unhandled_input(event: InputEvent) -> void:
$SceneViewport.push_input(event)
func _input(event):
if event.is_action_released("quit"):
get_tree().quit()
if event.is_action_released("right_click"):
if event.is_action_released("right_click") and not is_cursor_locked:
ActionState.current_action = (ActionState.current_action + 1) % 4
Input.set_custom_mouse_cursor(cursors[ActionState.current_action])

View File

@@ -123,18 +123,21 @@ func start_main_script(s):
ScriptBuilder.current_script = s
func _unhandled_input(event):
if event is InputEventMouseButton and event.is_action("interact"):
print (ego.position, pathfind.to_local(get_global_mouse_position()))
if event is InputEventMouseButton and Input.is_action_just_released("interact"):
# If a script is running and can't be interrupted, delegate to it
if ScriptBuilder.current_script and not ScriptBuilder.current_script.can_interrupt:
if ScriptBuilder.current_script.handle_input(event):
return
var root = get_node("/root/Node2D")
# If look cursor is active and we got here, no SetPiece handled the input
# so this is a room-wide look
if ActionState.current_action == ActionState.Action.LOOK:
_on_room_looked()
return
var path = NavigationServer2D.map_get_path(map, ego.position, pathfind.to_local(get_global_mouse_position()), true)
start_main_script(ScriptBuilder.init(ScriptBuilder.walk_path(ego, path)).can_interrupt().build(self, "_on_script_complete"))
pass
if ActionState.current_action == ActionState.Action.WALK:
var path = NavigationServer2D.map_get_path(map, ego.position, pathfind.to_local(get_global_mouse_position()), true)
start_main_script(ScriptBuilder.init(ScriptBuilder.walk_path(ego, path)).can_interrupt().build(self, "_on_script_complete"))
func _on_room_looked() -> void:
# Default room look description - override in room scripts

View File

@@ -49,7 +49,6 @@ func _process(delta):
if Geometry2D.is_point_in_polygon(to_local(get_global_mouse_position()), self.polygon):
if is_in == false:
is_in = true
print("ENTERED0", label)
emit_signal("entered", label)
else:
if is_in == true:
@@ -72,7 +71,12 @@ func _get_overlapping_setpieces() -> Array[SetPiece]:
func _input(event):
if not Engine.is_editor_hint():
if Geometry2D.is_point_in_polygon(to_local(get_global_mouse_position()), self.polygon):
if event is InputEventMouseButton and event.is_action("interact"):
if event is InputEventMouseButton and Input.is_action_just_released("interact"):
# Check if a script is running that shouldn't be interrupted
var script_builder = get_node_or_null("/root/Node2D/GameScript")
if script_builder and script_builder.current_script and not script_builder.current_script.can_interrupt:
return # Let the script handle the input
# Find all overlapping SetPieces and check if this one has highest priority
var overlapping = _get_overlapping_setpieces()
overlapping.append(self)

BIN
hourglass_icon.png LFS Normal file

Binary file not shown.

40
hourglass_icon.png.import Normal file
View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b8vin6hvl2ihd"
path="res://.godot/imported/hourglass_icon.png-8d78a8fc9e1e39f47135f2bd27e87cd0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://hourglass_icon.png"
dest_files=["res://.godot/imported/hourglass_icon.png-8d78a8fc9e1e39f47135f2bd27e87cd0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

1
hourglass_icon.png.uid Normal file
View File

@@ -0,0 +1 @@
uid://f4bd88e46ad74

View File

@@ -65,8 +65,6 @@ func _process(delta):
mouse_pos.x = clamp(mouse_pos.x, cam_top_left.x + margin, cam_bottom_right.x - label.size.x - margin)
mouse_pos.y = clamp(mouse_pos.y, cam_top_left.y, cam_bottom_right.y)
if $label.visible:
print("LABEL VISIBLE", $label.text)
# Update label position
global_position = mouse_pos

View File

@@ -19,29 +19,29 @@ func _on_forest_path_27_interacted() -> void:
func _on_pool_looked() -> void:
start_main_script(ScriptBuilder.init(
ScriptBuilder.say(ego, "The beautiful pool is lined with tall marble columns. Its crystal clear water looks very inviting.")
ScriptBuilder.narrate("The beautiful pool is lined with tall marble columns. Its crystal clear water looks very inviting.")
).build(self, "_on_script_complete"))
func _on_columns_looked() -> void:
start_main_script(ScriptBuilder.init(
ScriptBuilder.say(ego, "The marble columns flank the lovely pool.")
ScriptBuilder.narrate("The marble columns flank the lovely pool.")
).build(self, "_on_script_complete"))
func _on_stairs_looked() -> void:
start_main_script(ScriptBuilder.init(
ScriptBuilder.say(ego, "There are steps at the north end of the pool.")
ScriptBuilder.narrate("There are steps at the north end of the pool.")
).build(self, "_on_script_complete"))
func _on_ground_looked() -> void:
start_main_script(ScriptBuilder.init(
ScriptBuilder.say(ego, "You see nothing of importance on the ground.")
ScriptBuilder.narrate("You see nothing of importance on the ground.")
).build(self, "_on_script_complete"))
func _on_room_looked() -> void:
start_main_script(ScriptBuilder.init(
ScriptBuilder.say(ego, "The beautiful pool, with its elegant marble columns, has a wonderful setting in these woods. The water looks so cool and inviting; you're almost tempted to jump in.")
ScriptBuilder.narrate("The beautiful pool, with its elegant marble columns, has a wonderful setting in these woods. The water looks so cool and inviting; you're almost tempted to jump in.")
).build(self, "_on_script_complete"))