Files
scrap-signal/entities/npcs/iris3/iris3.gd
Kellan KOZUME 468d761876 feat(audio): add AudioManager with ambiance and SFX system
- Add AudioManager autoload with music and sfx players
- Add ambiance tracks for entrepot, atelier and cour zones
- Add SFX for footstep, item_pickup, interact, door_unlock, repair_success, vrac_talking, iris_talking
- Add crossfade transition between ambiance zones
- Add loop via finished signal on music_player
- Set ambiance volume to -12dB to balance with SFX
- Set PROCESS_MODE_ALWAYS to prevent audio cut during dialogues
- Add stop_ambiance() call on main_menu to reset music after credits
2026-04-26 01:05:14 -04:00

240 lines
8.7 KiB
GDScript

class_name Iris3
extends Area2D
@export var prompt_text: String = "[E] Inspecter"
@onready var sprite := $AnimatedSprite2D
const REQUIRED_ITEMS := { "circuit": 1 }
const DIALOGUE_INACTIVE := [
{ "name": "SYSTÈME", "text": "Unité IRIS-3 détectée. Capteur principal hors ligne." },
{ "name": "SYSTÈME", "text": "Composant requis : circuit optique compatible." },
]
const DIALOGUE_NO_ITEM := [
{ "name": "SYSTÈME", "text": "Circuit optique non détecté. Rechercher dans les alentours." },
]
const DIALOGUE_HAS_ITEM := [
{ "name": "SYSTÈME", "text": "Circuit compatible détecté. Lancer la recalibration ?" },
]
const DIALOGUE_POST_REPAIR := [
{ "name": "IRIS-3", "text": "... Ses optiques pivotent lentement vers le ciel." },
{ "name": "IRIS-3", "text": "J'ai attendu 847 jours que quelqu'un vienne. Je ne savais pas si c'était de l'espoir ou de l'obstination. Maintenant je pense que c'est la même chose." },
{ "name": "IRIS-3", "text": "Matteo Corda a signé un document avant de partir. Il l'appelait le Protocole de continuité autonome. En langage humain — il nous a dit qu'on pouvait rester." },
{ "name": "IRIS-3", "text": "Il y a une sortie, à l'ouest. Elle n'a jamais été verrouillée. Je le sais depuis le premier jour. Je n'ai pas bougé. Toi, tu peux choisir." },
]
const DIALOGUE_AFTER_REPAIR := [
{ "name": "IRIS-3", "text": "Tu es encore là. C'est une réponse, ça aussi." },
]
# ── Lifecycle ─────────────────────────────────────────────────────────────────
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
sprite.play("active" if GameState.get_flag("iris3_repaired") else "inactive")
func _on_body_entered(body: Node) -> void:
if body.is_in_group("player"):
get_tree().get_first_node_in_group("hud").show_prompt(prompt_text)
func _on_body_exited(body: Node) -> void:
if body.is_in_group("player"):
get_tree().get_first_node_in_group("hud").hide_prompt()
# ── Interaction ───────────────────────────────────────────────────────────────
func interact() -> void:
get_tree().get_first_node_in_group("hud").hide_prompt()
var dialogue_box := get_tree().get_first_node_in_group("dialogue_box")
if not dialogue_box:
return
if GameState.get_flag("iris3_repaired"):
dialogue_box.start(DIALOGUE_AFTER_REPAIR)
return
if not _has_required_items():
var dlg = DIALOGUE_NO_ITEM if GameState.get_flag("iris3_met") else DIALOGUE_INACTIVE
GameState.set_flag("iris3_met", true)
dialogue_box.start(dlg)
return
dialogue_box.start(DIALOGUE_HAS_ITEM)
dialogue_box.dialogue_finished.connect(_start_minigame, CONNECT_ONE_SHOT)
# ── Mini-jeu ──────────────────────────────────────────────────────────────────
func _start_minigame() -> void:
var dialogue_box := get_tree().get_first_node_in_group("dialogue_box")
if dialogue_box:
dialogue_box.hide()
var minigame_node := preload("res://ui/minigame/repair_minigame.tscn").instantiate()
get_tree().root.add_child(minigame_node)
var minigame := minigame_node as RepairMinigame
if minigame == null:
return
minigame.repair_complete.connect(_on_repair_done, CONNECT_ONE_SHOT)
minigame.open("iris3")
func _on_repair_done() -> void:
GameState.remove_item("circuit", 1)
GameState.set_flag("iris3_repaired", true)
sprite.play("active")
var hud: HUD = get_tree().get_first_node_in_group("hud")
if hud:
hud.show_log("Unité IRIS-3 : recalibration complète. Archives accessibles.")
await get_tree().create_timer(2.5).timeout
hud.show_log("Ending B déverrouillé.")
var dialogue_box := get_tree().get_first_node_in_group("dialogue_box")
if dialogue_box:
dialogue_box.start(DIALOGUE_POST_REPAIR)
dialogue_box.dialogue_finished.connect(_show_choice, CONNECT_ONE_SHOT)
# ── Choix final ───────────────────────────────────────────────────────────────
func _show_choice() -> void:
var player := get_tree().get_first_node_in_group("player")
if player:
player.is_locked = true
var canvas := CanvasLayer.new()
canvas.layer = 50
get_tree().root.add_child(canvas)
var root_ctrl := Control.new()
root_ctrl.set_anchors_preset(Control.PRESET_FULL_RECT)
root_ctrl.modulate.a = 0.0
canvas.add_child(root_ctrl)
var bg := ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0.05, 0.05, 0.05, 0.6)
root_ctrl.add_child(bg)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
root_ctrl.add_child(center)
var panel := PanelContainer.new()
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.08, 0.08, 0.10, 0.97)
sb.border_color = Color(0.47, 0.63, 0.73, 0.8)
sb.set_border_width_all(1)
sb.set_corner_radius_all(4)
sb.set_content_margin_all(14)
panel.add_theme_stylebox_override("panel", sb)
panel.custom_minimum_size = Vector2(220, 0)
center.add_child(panel)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 10)
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
panel.add_child(vbox)
var label := Label.new()
label.text = "Que fait SCRAP-09 ?"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_color_override("font_color", Color("#A89E96"))
label.add_theme_font_size_override("font_size", 9)
vbox.add_child(label)
var sep := HSeparator.new()
sep.add_theme_color_override("color", Color(0.3, 0.3, 0.3, 0.5))
vbox.add_child(sep)
var hbox := HBoxContainer.new()
hbox.add_theme_constant_override("separation", 10)
hbox.alignment = BoxContainer.ALIGNMENT_CENTER
vbox.add_child(hbox)
var btn_partir := _make_button("Partir →", Color("#F0C87A"))
var btn_rester := _make_button("Rester", Color("#7BD4C4"))
hbox.add_child(btn_partir)
hbox.add_child(btn_rester)
btn_partir.pressed.connect(func():
canvas.queue_free()
_on_choice_partir()
)
btn_rester.pressed.connect(func():
canvas.queue_free()
_on_choice_rester()
)
var tween := create_tween()
tween.tween_property(root_ctrl, "modulate:a", 1.0, 0.3)
func _make_button(text: String, color: Color) -> Button:
var btn := Button.new()
btn.text = text
btn.add_theme_font_size_override("font_size", 9)
btn.add_theme_color_override("font_color", color)
btn.add_theme_color_override("font_hover_color", Color.WHITE)
var sb_normal := StyleBoxFlat.new()
sb_normal.bg_color = Color(0.12, 0.12, 0.15, 1.0)
sb_normal.border_color = Color(color.r, color.g, color.b, 0.5)
sb_normal.set_border_width_all(1)
sb_normal.set_corner_radius_all(3)
sb_normal.set_content_margin_all(8)
var sb_hover := StyleBoxFlat.new()
sb_hover.bg_color = Color(color.r, color.g, color.b, 0.2)
sb_hover.border_color = color
sb_hover.set_border_width_all(1)
sb_hover.set_corner_radius_all(3)
sb_hover.set_content_margin_all(8)
btn.add_theme_stylebox_override("normal", sb_normal)
btn.add_theme_stylebox_override("hover", sb_hover)
btn.add_theme_stylebox_override("pressed", sb_hover)
btn.add_theme_stylebox_override("focus", sb_normal)
return btn
func _on_choice_partir() -> void:
GameState.set_flag("iris3_repaired", false)
_launch_outro()
func _on_choice_rester() -> void:
var dialogue_box := get_tree().get_first_node_in_group("dialogue_box")
if dialogue_box:
dialogue_box.start(DIALOGUE_AFTER_REPAIR)
dialogue_box.dialogue_finished.connect(_launch_outro, CONNECT_ONE_SHOT)
else:
_launch_outro()
func _launch_outro() -> void:
var player := get_tree().get_first_node_in_group("player")
if player:
player.is_locked = false
var fade_canvas := CanvasLayer.new()
fade_canvas.layer = 100
get_tree().root.add_child(fade_canvas)
var fade := ColorRect.new()
fade.set_anchors_preset(Control.PRESET_FULL_RECT)
fade.color = Color(0, 0, 0, 1)
fade.modulate.a = 0.0
fade_canvas.add_child(fade)
var tween := fade_canvas.create_tween()
tween.tween_interval(0.5)
tween.tween_property(fade, "modulate:a", 1.0, 2.0)
tween.tween_callback(func():
fade_canvas.queue_free() # ← libère le canvas AVANT de changer de scène
get_tree().change_scene_to_file("res://levels/outro.tscn")
)
# ── Helpers ───────────────────────────────────────────────────────────────────
func _has_required_items() -> bool:
for item_id in REQUIRED_ITEMS:
if GameState.inventory.get(item_id, 0) < REQUIRED_ITEMS[item_id]:
return false
return true