extends Node2D

const ActorScript = preload("res://scripts/prologue_actor.gd")
const DialogueScript = preload("res://scripts/dialogue_view.gd")
const PortraitScript = preload("res://scripts/portrait_view.gd")
const GridScript = preload("res://scripts/map_overlay.gd")
const AudioScript = preload("res://scripts/audio_director.gd")
const IDS := ["ganoes", "commander", "fiddler", "surly"]
const NAMES := {"ganoes":"Ganoes Paran", "commander":"Commander", "fiddler":"Bridgeburner", "surly":"Surly"}
const EMOTIONS := {"ganoes":["neutral","proud","scowl","surprised"], "commander":["neutral","grimace"], "fiddler":["neutral","wry"], "surly":["neutral","cold_smile"]}

var story: Dictionary
var events: Array
var actors: Dictionary = {}
var expressions := {"ganoes":"neutral","commander":"neutral","fiddler":"neutral","surly":"neutral"}
var world: Node2D
var actor_layer: Node2D
var dialogue: Control
var map_overlay: Node2D
var smoke_material: ShaderMaterial
var vane_head: Sprite2D
var overlay_layer: CanvasLayer
var intro_layer: CanvasLayer
var gallery_layer: CanvasLayer
var gallery_portraits: Dictionary = {}
var gallery_actors: Array = []
var gallery_names: Dictionary = {}
var gallery_emotion_names: Dictionary = {}
var guard_previews: Array = []
var gallery_heading: Label
var book_panel: Panel
var book_body: RichTextLabel
var status: Label
var current_event := -1
var current_paragraph := 4
var gallery_open := false
var book_open := false
var selected_character := 0
var gallery_emotions := [0,0,0,0]
var gallery_direction := 0
var guard_mode := false
var autoplay := false
var show_status := true
var skip_intro := false
var in_intro := false
var test_mode := false
var review_film := false
var capture_review := false
var map_review := false
var emotion_review := false
var gallery_running := false
var test_captures: Dictionary = {}
var elapsed := 0.0
var dialogue_count := 0
var completed_events := 0
var character_data: Dictionary
var floor_layer: TileMapLayer
var map_layout: Dictionary
var quiet_floor := true
var audio
var audio_panel: PanelContainer
var audio_open := false
var audio_review := false
var audio_candidate := ""
var vane_cues: Dictionary = {}

func _ready() -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS
	texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
	test_mode = "--smoke-test" in OS.get_cmdline_user_args()
	review_film = "--review-film" in OS.get_cmdline_user_args()
	map_review = "--review-map" in OS.get_cmdline_user_args()
	emotion_review = "--review-emotions" in OS.get_cmdline_user_args()
	audio_review = "--review-audio" in OS.get_cmdline_user_args()
	for argument in OS.get_cmdline_user_args():
		if argument.begins_with("--audio-music="):
			audio_candidate = argument.trim_prefix("--audio-music=")
	capture_review = "--capture-review" in OS.get_cmdline_user_args() or map_review
	if review_film:
		show_status = false
	# Normal play waits for input; explicit automated review/test modes advance.
	autoplay = test_mode or review_film
	story = JSON.parse_string(FileAccess.get_file_as_string("res://content/prologue.json"))
	character_data = JSON.parse_string(FileAccess.get_file_as_string("res://content/characters.json"))
	events = story["events"]
	_build_world()
	_build_ui()
	_build_gallery()
	if not map_review and not emotion_review and not test_mode:
		audio = AudioScript.new()
		add_child(audio)
		audio_panel = audio.build_panel(overlay_layer)
		for actor in actors.values():
			actor.foot_contact.connect(audio.footstep)
		dialogue.character_revealed.connect(_on_audio_character)
		audio.start(audio_candidate)
	if "--review-audio-panel" in OS.get_cmdline_user_args():
		capture_review = true
		audio_open = true
		_pause_for_review()
		await _capture("audio_controls_v1")
		audio.shutdown()
		await get_tree().process_frame
		await get_tree().process_frame
		get_tree().quit()
		return
	if audio_review:
		await _review_audio_scene()
		audio.shutdown()
		await get_tree().process_frame
		await get_tree().process_frame
		get_tree().quit()
		return
	if emotion_review:
		await _review_portrait_transitions()
		get_tree().quit()
		return
	if map_review:
		await _capture_map_review()
		get_tree().quit()
		return
	_run_story()

func _on_audio_character(character: String) -> void:
	if audio and not character.strip_edges().is_empty():
		audio.text_tick()

func _advance_dialogue() -> void:
	if dialogue.restore_presentation():
		return
	if audio and dialogue.active:
		audio.play_effect("ui_advance",0.0,1.0,true)
	dialogue.advance()

func _review_audio_scene() -> void:
	# Fixed staging for A/B/C listening; separate from the full book timeline.
	status.hide()
	audio.ticks_enabled = false
	for bus in audio.levels:
		audio.apply_level(bus,{"Master":0.8,"Music":0.75,"Ambience":0.7,"Effects":0.65,"Dialogue":0.5}[bus],false)
	_label(overlay_layer,"AUDIO STUDY / same staging for A, B and C",Vector2(14,12),13)
	_label(overlay_layer,"Wind · stone footsteps · light equipment · rusty vane",Vector2(14,31),11)
	_label(overlay_layer,"Listening study: condensed staging, no added dialogue",Vector2(14,334),11)
	actors.ganoes.pose("tiptoe")
	await _wait(1.0)
	await actors.commander.walk_route([[560,184],[496,184],[496,164],[368,164]],84.0)
	actors.commander.face("north")
	await _wait(1.2)
	audio.play_effect("vane_creak",-2.0)
	await _wait(1.5)
	await actors.fiddler.walk_route([[560,184],[496,184],[496,164],[432,164]],124.0,"run")
	actors.fiddler.face("north")
	await _wait(4.0)
	await _arrival()
	await _wait(2.0)
	await actors.fiddler.walk_route([[496,164],[496,184],[560,184],[560,176]],144.0,"run")
	await actors.fiddler.disappear()
	await _wait(2.0)
	audio.play_effect("cloth_shift",-4.0)
	await _wait(maxf(0.1,30.0-elapsed))

func _review_portrait_transitions() -> void:
	# A short visual study using the actual gallery and dialogue components.
	# Preview captions are separate from the book's scripted dialogue.
	status.hide()
	gallery_open = true
	_pause_for_review()
	gallery_heading.text = "EXPRESSION BLENDS / 0.20 seconds / aligned portraits"
	await get_tree().create_timer(0.8, true).timeout
	for emotions in [[1,1,1,1], [2,0,0,0], [3,1,1,1], [0,0,0,0]]:
		gallery_emotions.assign(emotions)
		_refresh_gallery()
		gallery_heading.text = "EXPRESSION BLENDS / 0.20 seconds / aligned portraits"
		await get_tree().create_timer(0.1, true).timeout
		await _capture("portrait_blend_gallery_midpoint")
		await get_tree().create_timer(0.8, true).timeout
	gallery_open = false
	_pause_for_review()
	for id in actors:
		actors[id].position = Vector2(map_layout["pixel_stops"][id][0], map_layout["pixel_stops"][id][1])
		actors[id].show()
		actors[id].face("north")
	_label(overlay_layer, "EXPRESSION TRANSITION STUDY / 0.20 s", Vector2(14,14), 13)
	dialogue.auto_advance = false
	for step in [["ganoes","neutral"], ["ganoes","proud"], ["ganoes","surprised"], ["commander","neutral"], ["commander","grimace"], ["ganoes","scowl"], ["ganoes","neutral"]]:
		dialogue.present(step[0], NAMES[step[0]], "Expression preview: " + step[1].replace("_", " "), step[1])
		await get_tree().create_timer(0.9, true).timeout
		await _capture("portrait_blend_dialogue_" + step[0] + "_" + step[1])
	dialogue.close_immediately()
	await get_tree().create_timer(0.3, true).timeout

func _capture_map_review() -> void:
	# A static staging render for coordinate-based art feedback.
	status.hide()
	for id in actors:
		actors[id].position = Vector2(map_layout["pixel_stops"][id][0],map_layout["pixel_stops"][id][1])
		actors[id].show()
		actors[id].face("north")
	actors.ganoes.pose("tiptoe")
	actors.ganoes.visual.frame = 2
	actors.ganoes.visual.pause()
	actors.commander.face("north")
	actors.fiddler.face("north")
	await get_tree().process_frame
	await _capture("wall_pose_review")
	map_overlay.grid_visible = true
	map_overlay.queue_redraw()
	await _capture("map_coordinates")
	map_overlay.grid_visible = false
	map_overlay.queue_redraw()
	gallery_open = true
	_pause_for_review()
	await _capture("cast_gallery")

func _label(parent: Node, text: String, pos: Vector2, size: int = 12) -> Label:
	var label := Label.new()
	label.text = text
	label.position = pos
	label.add_theme_font_size_override("font_size", size)
	label.add_theme_color_override("font_color", Color("e7dfcd"))
	label.mouse_filter = Control.MOUSE_FILTER_IGNORE
	parent.add_child(label)
	return label

func _apply_floor() -> void:
	for y in range(5,11):
		for x in range(20):
			var tile: int = int(map_layout["tiles"][y][x])
			if quiet_floor and tile < 4:
				floor_layer.set_cell(Vector2i(x,y),1,Vector2i(tile%2,tile/2))
			else:
				floor_layer.set_cell(Vector2i(x,y),0,Vector2i(tile%4,tile/4))

func _atlas(texture: Texture2D, region: Rect2) -> AtlasTexture:
	var result := AtlasTexture.new()
	result.atlas = texture
	result.region = region
	return result

func _build_world() -> void:
	world = Node2D.new()
	world.process_mode = Node.PROCESS_MODE_PAUSABLE
	add_child(world)
	var backdrop := TextureRect.new()
	backdrop.texture = load("res://art/environment/city_band_v4.png")
	backdrop.position = Vector2.ZERO
	backdrop.size = Vector2(640, 128)
	backdrop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
	backdrop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
	backdrop.clip_contents = true
	backdrop.modulate = Color(0.73, 0.75, 0.80)
	world.add_child(backdrop)
	map_layout = JSON.parse_string(FileAccess.get_file_as_string("res://content/prologue_map.json"))
	var tile_set := TileSet.new()
	tile_set.tile_size = Vector2i(32, 32)
	var source := TileSetAtlasSource.new()
	source.texture = load("res://art/tiles/mocks_hold_tiles_v1.png")
	source.texture_region_size = Vector2i(32, 32)
	for y in range(4):
		for x in range(4):
			source.create_tile(Vector2i(x,y))
	tile_set.add_source(source, 0)
	var quiet_source := TileSetAtlasSource.new()
	quiet_source.texture = load("res://art/tiles/mocks_hold_floor_quiet_v2.png")
	quiet_source.texture_region_size = Vector2i(32,32)
	for y in range(2):
		for x in range(2):
			quiet_source.create_tile(Vector2i(x,y))
	tile_set.add_source(quiet_source, 1)
	floor_layer = TileMapLayer.new()
	floor_layer.tile_set = tile_set
	world.add_child(floor_layer)
	var wall_layer := TileMapLayer.new()
	wall_layer.tile_set = tile_set
	wall_layer.z_index = 1
	world.add_child(wall_layer)
	for y in range(11):
		for x in range(20):
			var tile: int = int(map_layout["tiles"][y][x])
			if tile < 0:
				continue
			var layer := floor_layer if y >= 5 else wall_layer
			layer.set_cell(Vector2i(x,y), 0, Vector2i(tile % 4, tile / 4))
	_apply_floor()
	var portal := Sprite2D.new()
	portal.texture = load("res://art/props/citadel_portal_v1.png")
	portal.centered = false
	portal.position = Vector2(map_layout.portal_position[0], map_layout.portal_position[1])
	portal.z_index = 2
	world.add_child(portal)
	var vane: Texture2D = load("res://art/props/mocks_vane_v2.png")
	var pole := Sprite2D.new()
	pole.texture = _atlas(vane, Rect2(0,40,48,72))
	pole.centered = false
	pole.position = Vector2(216,72)
	pole.z_index = 4
	world.add_child(pole)
	vane_head = Sprite2D.new()
	vane_head.texture = _atlas(vane, Rect2(0,0,48,40))
	vane_head.centered = false
	vane_head.offset = Vector2(-24,-40)
	vane_head.position = Vector2(240,72)
	vane_head.z_index = 4
	world.add_child(vane_head)
	actor_layer = Node2D.new()
	actor_layer.y_sort_enabled = true
	actor_layer.z_index = 2
	world.add_child(actor_layer)
	var ganoes_stop: Array = map_layout["pixel_stops"]["ganoes"]
	var positions := {
		"ganoes": Vector2(ganoes_stop[0], ganoes_stop[1]),
		"commander": Vector2(560,176), "fiddler": Vector2(560,176),
		"surly": Vector2(560,176),
		"claw_left": Vector2(560,176), "claw_right": Vector2(560,176),
	}
	for id in positions:
		var actor = ActorScript.new()
		actor_layer.add_child(actor)
		actor.configure("claw" if id.begins_with("claw") else id)
		actor.position = positions[id]
		actor.visible = id == "ganoes"
		actors[id] = actor
	var smoke := ColorRect.new()
	smoke.position = Vector2.ZERO
	smoke.size = Vector2(640,360)
	smoke.mouse_filter = Control.MOUSE_FILTER_IGNORE
	smoke.z_index = 20
	smoke_material = ShaderMaterial.new()
	smoke_material.shader = load("res://art/smoke.gdshader")
	smoke_material.set_shader_parameter("strength", 0.14)
	smoke.material = smoke_material
	world.add_child(smoke)
	map_overlay = GridScript.new()
	map_overlay.z_index = 30
	world.add_child(map_overlay)

func _build_ui() -> void:
	overlay_layer = CanvasLayer.new()
	overlay_layer.layer = 10
	add_child(overlay_layer)
	dialogue = DialogueScript.new()
	overlay_layer.add_child(dialogue)
	dialogue.auto_advance = autoplay
	dialogue.test_mode = test_mode
	dialogue.review_film = review_film
	status = _label(overlay_layer, "", Vector2(10,4), 10)
	status.modulate = Color(1,1,1,0.75)
	status.visible = show_status
	book_panel = Panel.new()
	book_panel.position = Vector2(24,32)
	book_panel.size = Vector2(592,296)
	var style := StyleBoxFlat.new()
	style.bg_color = Color("171c26")
	style.border_color = Color("b4a484")
	style.set_border_width_all(1)
	book_panel.add_theme_stylebox_override("panel", style)
	overlay_layer.add_child(book_panel)
	_label(book_panel, "BOOK REFERENCE / current original paragraph / B closes", Vector2(16,10), 12)
	book_body = RichTextLabel.new()
	book_body.position = Vector2(16,40)
	book_body.size = Vector2(560,240)
	book_body.add_theme_font_size_override("normal_font_size",14)
	book_body.add_theme_color_override("default_color",Color("ece3d4"))
	book_body.add_theme_constant_override("line_separation",4)
	book_panel.add_child(book_body)
	book_panel.hide()

func _build_gallery() -> void:
	gallery_layer = CanvasLayer.new()
	gallery_layer.layer = 30
	gallery_layer.process_mode = Node.PROCESS_MODE_ALWAYS
	add_child(gallery_layer)
	var background := ColorRect.new()
	background.color = Color("1c222c")
	background.size = Vector2(640,360)
	gallery_layer.add_child(background)
	gallery_heading = _label(gallery_layer, "BUST AND ANIMATION CANDIDATES / 160 x 192 portraits", Vector2(10,5),13)
	for i in range(4):
		var id: String = IDS[i]
		var portrait = PortraitScript.new()
		portrait.position = Vector2(i*160,30)
		portrait.size = Vector2(160,192)
		portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
		portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
		gallery_layer.add_child(portrait)
		gallery_portraits[id] = portrait
		gallery_names[id] = _label(gallery_layer, NAMES[id], Vector2(i*160+8,228),13)
		gallery_emotion_names[id] = _label(gallery_layer, "neutral", Vector2(i*160+8,248),11)
		var actor = ActorScript.new()
		gallery_layer.add_child(actor)
		actor.configure(id)
		actor.position = Vector2(i*160+80,336)
		gallery_actors.append(actor)
	for x in [280,360]:
		var guard = ActorScript.new()
		gallery_layer.add_child(guard)
		guard.configure("claw")
		guard.position = Vector2(x,260)
		guard_previews.append(guard)
	_label(gallery_layer,"1-4 select  Tab emotion  Arrows facing  V fiddle walk/run  5 guards  F2 return",Vector2(10,343),10)
	_refresh_gallery()
	gallery_layer.hide()

func _refresh_gallery() -> void:
	var direction: String = ["south","west","east","north"][gallery_direction]
	for i in range(4):
		var id: String = IDS[i]
		var emotion: String = EMOTIONS[id][gallery_emotions[i]]
		gallery_portraits[id].show_expression(id, load(character_data[id]["portraits"][emotion]), not test_mode)
		gallery_portraits[id].visible = not guard_mode
		gallery_names[id].text = ("› " if i == selected_character else "") + NAMES[id]
		gallery_names[id].visible = not guard_mode
		gallery_emotion_names[id].text = emotion.replace("_"," ") + (" / RUN" if gallery_running and id == "fiddler" else "")
		gallery_emotion_names[id].visible = not guard_mode
		gallery_actors[i].visible = not guard_mode
		gallery_actors[i].visual.play(("run_" if gallery_running and id == "fiddler" else "walk_")+direction)
	for guard in guard_previews:
		guard.visible = guard_mode
		guard.visual.play("walk_"+direction)
	gallery_heading.text = "CLAW ACOLYTES / one reusable faceless model" if guard_mode else "BUST AND ANIMATION CANDIDATES / 160 x 192 portraits"

func _pause_for_review() -> void:
	get_tree().paused = gallery_open or book_open or audio_open
	if audio_panel:
		audio_panel.visible = audio_open
	if audio:
		audio.pause_effects(get_tree().paused)
	gallery_layer.visible = gallery_open
	book_panel.visible = book_open
	dialogue.visible = dialogue.active and not gallery_open and not book_open and not audio_open
	if book_open:
		book_body.text = story["paragraphs"][current_paragraph]

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey and event.pressed and not event.echo:
		match event.keycode:
			KEY_F2:
				audio_open = false
				gallery_open = not gallery_open
				book_open = false
				_pause_for_review()
			KEY_B:
				audio_open = false
				book_open = not book_open
				gallery_open = false
				_pause_for_review()
			KEY_F3:
				if audio:
					audio_open = not audio_open
					gallery_open = false
					book_open = false
					_pause_for_review()
			KEY_U:
				if audio:
					audio.toggle_mute()
			KEY_F11:
				DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN else DisplayServer.WINDOW_MODE_FULLSCREEN)
			KEY_R:
				get_tree().paused = false
				get_tree().reload_current_scene()
			KEY_ESCAPE:
				audio_open = false
				gallery_open = false
				book_open = false
				_pause_for_review()
			KEY_G:
				map_overlay.grid_visible = not map_overlay.grid_visible
				map_overlay.queue_redraw()
			KEY_H:
				show_status = not show_status
				status.visible = show_status
			KEY_L:
				dialogue.large_portrait = not dialogue.large_portrait
				dialogue.refresh_layout()
			KEY_M:
				quiet_floor = not quiet_floor
				_apply_floor()
			KEY_A:
				autoplay = not autoplay
				dialogue.auto_advance = autoplay
			KEY_SPACE, KEY_ENTER:
				if gallery_open or book_open or audio_open:
					return
				if in_intro:
					skip_intro = true
				else:
					_advance_dialogue()
		if gallery_open:
			if event.keycode >= KEY_1 and event.keycode <= KEY_4:
				selected_character = event.keycode-KEY_1
				guard_mode = false
			elif event.keycode == KEY_5:
				guard_mode = not guard_mode
			elif event.keycode == KEY_TAB:
				gallery_emotions[selected_character] = (gallery_emotions[selected_character]+1)%EMOTIONS[IDS[selected_character]].size()
			elif event.keycode == KEY_V:
				gallery_running = not gallery_running
			elif event.keycode in [KEY_LEFT,KEY_RIGHT,KEY_UP,KEY_DOWN]:
				gallery_direction = {KEY_DOWN:0,KEY_LEFT:1,KEY_RIGHT:2,KEY_UP:3}[event.keycode]
			_refresh_gallery()
	elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT and not gallery_open and not book_open and not audio_open:
		if in_intro:
			skip_intro = true
		else:
			_advance_dialogue()

func _process(delta: float) -> void:
	if not gallery_open and not book_open and not audio_open:
		elapsed += delta
		vane_head.scale.x = 0.92 + sin(elapsed*0.8)*0.08
	status.text = "DRAFT / F2 cast  F3 audio  U mute  G grid  B book  A auto  R replay  H hide"
	if capture_review and not map_review and not emotion_review:
		if dialogue.active and dialogue.text_view.visible_characters == -1:
			_capture("dialogue_" + dialogue.speaker_id + "_" + dialogue.expression)
		var boy = actors["ganoes"].visual
		if boy.animation == "tiptoe" and boy.frame == 2:
			_capture("ganoes_tiptoe_raised")

func _wait(seconds: float) -> void:
	var duration := minf(seconds,0.02) if test_mode else seconds
	var waited := 0.0
	while waited < duration:
		await get_tree().process_frame
		if not gallery_open and not book_open and not audio_open:
			waited += get_process_delta_time()

func _capture(name: String) -> void:
	if not (test_mode or capture_review) or test_captures.has(name):
		return
	test_captures[name] = true
	await RenderingServer.frame_post_draw
	var folder := ProjectSettings.globalize_path("res://art/previews/prologue")
	DirAccess.make_dir_recursive_absolute(folder)
	var error := get_viewport().get_texture().get_image().save_png(folder.path_join(name+".png"))
	if error != OK:
		push_error("Capture failed: "+name)

func _establish() -> void:
	intro_layer = CanvasLayer.new()
	intro_layer.layer = 5
	add_child(intro_layer)
	var art := TextureRect.new()
	art.mouse_filter = Control.MOUSE_FILTER_IGNORE
	# Ignore the source minimum size before assigning texture/size; fit the whole composition.
	art.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
	art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
	art.texture = load("res://art/source/mocks_hold_background_v2.png")
	art.size = Vector2(640,360)
	intro_layer.add_child(art)
	var shade := ColorRect.new()
	shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
	shade.position = Vector2(0,236)
	shade.size = Vector2(640,124)
	shade.color = Color(0.04,0.05,0.08,0.85)
	intro_layer.add_child(shade)
	_label(intro_layer,"GARDENS OF THE MOON",Vector2(24,245),22)
	_label(intro_layer,"PROLOGUE  /  MOCK'S HOLD",Vector2(24,276),14)
	_label(intro_layer,"1154th Year of Burn's Sleep  •  96th Year of the Malazan Empire",Vector2(24,300),12)
	_label(intro_layer,"The Last Year of Emperor Kellanved's Reign",Vector2(24,320),12)
	var continue_hint := _label(intro_layer,"▼",Vector2(604,337),12)
	continue_hint.modulate = Color("e8d8b4")
	in_intro = true
	await _capture("opening_establishing")
	var time := 0.0
	# Only explicit film/test runs time out. Normal play always waits for confirmation,
	# even if the player switches dialogue Auto on while viewing the artwork.
	while not skip_intro:
		await get_tree().process_frame
		if not gallery_open and not book_open and not audio_open:
			time += get_process_delta_time()
			if (test_mode or review_film) and time >= (0.03 if test_mode else 3.0):
				break
	var tween := create_tween()
	tween.tween_property(art,"modulate:a",0.0,0.02 if test_mode else 0.65)
	tween.parallel().tween_property(shade,"modulate:a",0.0,0.02 if test_mode else 0.65)
	await tween.finished
	intro_layer.queue_free()
	in_intro = false
	await _capture("map_initial")

func _enter_from_door(id: String, delay: float, route: Array, speed: float) -> void:
	await _wait(delay)
	actors[id].position = Vector2(560,176)
	await actors[id].walk_route(route,speed)
	actors[id].face("north")

func _arrival() -> void:
	if audio:
		audio.set_tension(true)
	var speed := 800.0 if test_mode else 76.0
	# Form up in front of the Commander, not beneath Fiddler's old position.
	var surly_stop: Array = map_layout["pixel_stops"]["surly"]
	var left_stop: Array = map_layout["pixel_stops"]["claw_left"]
	var right_stop: Array = map_layout["pixel_stops"]["claw_right"]
	_enter_from_door("surly",0.0,[[560,224],[surly_stop[0],224],surly_stop],speed)
	# The lower approach lanes keep the acolytes clear of Surly before they turn in.
	_enter_from_door("claw_left",0.65,[[560,264],[left_stop[0],264],left_stop],speed)
	await _enter_from_door("claw_right",1.3,[[560,280],[right_stop[0],280],right_stop],speed)
	while actors.surly.walking or actors.claw_left.walking:
		await get_tree().process_frame
	_face_surly_party_towards_commander()
	await _capture("surly_arrival_map")

func _face_surly_party_towards_commander() -> void:
	for id in ["surly", "claw_left", "claw_right"]:
		var offset: Vector2 = actors.commander.position - actors[id].position
		var direction: String
		if absf(offset.x) > absf(offset.y):
			direction = "east" if offset.x > 0 else "west"
		else:
			direction = "south" if offset.y > 0 else "north"
		actors[id].face(direction)

func _exit_through_door(id: String, delay: float, route: Array, speed: float) -> void:
	await _wait(delay)
	await actors[id].walk_route(route,speed)
	# Finish each exit independently, as soon as that actor crosses the threshold.
	await actors[id].disappear()

func _departure() -> void:
	if audio:
		audio.set_tension(false)
	var speed := 800.0 if test_mode else 76.0
	var leader_time: float = (absf(actors.surly.position.y-216.0)+560.0-actors.surly.position.x+40.0)/speed
	for index in range(3):
		var id: String = ["surly","claw_left","claw_right"][index]
		var start: Vector2 = actors[id].position
		var route := [[start.x,216],[560,216],[560,176]]
		var travel_time := (absf(start.y-216.0)+560.0-start.x+40.0)/speed
		# Arrival times, rather than equal start delays, keep the shared lane clear.
		var delay := maxf(0.0,leader_time+index*0.65-travel_time)
		_exit_through_door(id,delay,route,speed)
	while actors.surly.visible or actors.claw_left.visible or actors.claw_right.visible:
		await get_tree().process_frame

func _cue_paragraph_audio(paragraph: int) -> void:
	if not audio or vane_cues.has(paragraph):
		return
	vane_cues[paragraph] = true
	var text: String = story["paragraphs"][paragraph].to_lower()
	if ("vane" in text and ("squeal" in text or "groan" in text)) or ("iron demon" in text and "groan" in text):
		audio.play_effect("vane_creak",-2.0)

func _run_story() -> void:
	for index in range(events.size()):
		current_event = index
		var event: Dictionary = events[index]
		if event.has("source_paragraph"):
			current_paragraph = int(event.source_paragraph)
			_cue_paragraph_audio(current_paragraph)
		match event.kind:
			"establish":
				await _establish()
			"move":
				await actors[event.actor].walk_route(event.points,800.0 if test_mode else float(event.get("speed",82.0)),event.get("gait","walk"))
			"face":
				actors[event.actor].face(event.direction)
			"pose":
				actors[event.actor].pose(event.pose)
				if audio and event.pose == "lean":
					audio.play_effect("armour_shift",-4.0)
			"hide":
				await actors[event.actor].disappear()
			"wait":
				await _wait(float(event.seconds))
			"smoke":
				var tween := create_tween()
				tween.tween_property(smoke_material,"shader_parameter/strength",float(event.strength),0.02 if test_mode else float(event.duration))
				await tween.finished
			"arrival":
				await _arrival()
			"departure":
				await _departure()
			"expression":
				expressions[event.actor] = event.expression
			"beat":
				pass # Source-only beat: staging and audio, never a narration box.
			"say":
				dialogue_count += 1
				expressions[event.speaker] = event.expression
				var display_name: String = NAMES[event.speaker]
				if event.speaker == "surly" and current_paragraph > 51:
					display_name = "Laseen"
				dialogue.present(event.speaker,display_name,event.text,event.expression)
				if not capture_review:
					await _capture("dialogue_"+event.speaker+"_"+event.expression)
				if test_mode and event.text.length() > 180:
					await _capture("long_dialogue_page")
				if test_mode and not test_captures.has("floor_dense") and event.speaker == "commander":
					var was_active: bool = dialogue.active
					dialogue.hide()
					quiet_floor = false
					_apply_floor()
					await _capture("floor_dense")
					quiet_floor = true
					_apply_floor()
					await _capture("floor_quiet")
					dialogue.visible = was_active and dialogue.active
				if test_mode and not test_captures.has("ui_compact") and event.speaker == "ganoes":
					dialogue.large_portrait = false
					dialogue.refresh_layout()
					await _capture("ui_compact")
					dialogue.large_portrait = true
					dialogue.refresh_layout()
					book_open = true
					_pause_for_review()
					await _capture("book_reference")
					book_open = false
					_pause_for_review()
				if dialogue.active:
					await dialogue.completed
			"end":
				await _finish()
		completed_events = index+1
	if test_mode:
		print("PROLOGUE_TEST_OK cues=",completed_events," dialogue_paragraphs=",dialogue_count," captures=",test_captures.size())
		get_tree().quit()
	elif review_film:
		print("PROLOGUE_REVIEW_FILM_OK dialogue_paragraphs=",dialogue_count)
		if audio:
			audio.shutdown()
			await get_tree().process_frame
			await get_tree().process_frame
		get_tree().quit()

func _finish() -> void:
	dialogue.close_immediately()
	await _capture("ending_smoke")
	if review_film:
		await _wait(2.0)
		return
	if test_mode:
		map_overlay.grid_visible = true
		map_overlay.queue_redraw()
		await _capture("map_coordinates")
		map_overlay.grid_visible = false
		gallery_open = true
		_pause_for_review()
		await _capture("cast_gallery")
		guard_mode = true
		_refresh_gallery()
		await _capture("guard_gallery")
		guard_mode = false
		gallery_open = false
		_pause_for_review()
		return
	var end_layer := CanvasLayer.new()
	end_layer.layer = 6
	add_child(end_layer)
	var panel := ColorRect.new()
	panel.position = Vector2(112,108)
	panel.size = Vector2(416,132)
	panel.color = Color(0.06,0.07,0.10,0.96)
	end_layer.add_child(panel)
	_label(end_layer,"END OF PROLOGUE",Vector2(208,128),20)
	_label(end_layer,"Candidate assembly / artwork and staging under review",Vector2(140,165),12)
	_label(end_layer,"R: replay     F2: cast and animation gallery",Vector2(176,198),12)
